generated from john/python-template
UI style refresh with Gemini's help
This commit is contained in:
@@ -2,23 +2,28 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
from urllib.parse import urlencode
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from nicegui import ui
|
||||
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentError
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.db.models import JobSource, Source
|
||||
from transcription.services.documents import DocumentError, DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.transcription import TranscriptionService
|
||||
from transcription.services.transcription import TranscriptionNotFoundError
|
||||
from transcription.services.transcription import (
|
||||
TranscriptionNotFoundError,
|
||||
TranscriptionService,
|
||||
)
|
||||
from transcription.ui.components.app_shell import render_navigation_header
|
||||
from transcription.ui.components.cards import archival_card
|
||||
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 ...db.session import SessionFactoryDep
|
||||
|
||||
@@ -28,6 +33,7 @@ def register_page() -> None:
|
||||
|
||||
@ui.page("/sources")
|
||||
async def sources_page(request: Request, session_factory: SessionFactoryDep) -> None:
|
||||
apply_archival_theme()
|
||||
sources_service = TranscriptionService(session_factory=session_factory)
|
||||
jobs_service = JobService(session_factory=session_factory)
|
||||
documents_service = DocumentService(session_factory=session_factory)
|
||||
@@ -60,115 +66,134 @@ def register_page() -> None:
|
||||
else:
|
||||
sources = list(sorted(await sources_service.list_sources(), key=lambda item: (item.document_id, item.page_number)))
|
||||
except DocumentError:
|
||||
ui.label("Document not found").classes("text-h6 text-negative")
|
||||
ui.label("Document not found").classes("text-h6 text-red-800 p-4")
|
||||
return
|
||||
except ValueError:
|
||||
ui.label("Job not found").classes("text-h6 text-negative")
|
||||
ui.label("Job not found").classes("text-h6 text-red-800 p-4")
|
||||
return
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Load failed", operation="sources.list")
|
||||
return
|
||||
|
||||
if document_name is not None:
|
||||
ui.label(f"Sources for {document_name}").classes("text-h5 text-weight-medium")
|
||||
elif job_label is not None:
|
||||
ui.label(f"Sources for Job {job_label}").classes("text-h5 text-weight-medium")
|
||||
else:
|
||||
ui.label("Sources").classes("text-h5 text-weight-medium")
|
||||
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"):
|
||||
if document_name is not None:
|
||||
header_title = f"Sources: {document_name}"
|
||||
elif job_label is not None:
|
||||
header_title = f"Sources for Job {job_label}"
|
||||
else:
|
||||
header_title = "Archival Source Media"
|
||||
|
||||
with ui.row().classes("w-full items-center gap-2"):
|
||||
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")
|
||||
page_header(header_title)
|
||||
|
||||
if not sources:
|
||||
ui.label("No sources added yet.").classes("text-body2 vibe-text-muted")
|
||||
return
|
||||
|
||||
with ui.column().classes("w-full gap-2"):
|
||||
for source in sources:
|
||||
detail_path = _source_detail_path(source_id=source.id, document_id=document_id, job_id=job_id)
|
||||
with ui.card().classes("w-full") as card:
|
||||
card.on(
|
||||
"click",
|
||||
lambda _=None, route=detail_path: ui.navigate.to(route),
|
||||
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"
|
||||
)
|
||||
card.classes("cursor-pointer")
|
||||
with ui.row().classes("w-full items-center justify-between"):
|
||||
with ui.column().classes("gap-1"):
|
||||
ui.label(f"Page {source.page_number}: {source.upload_name}").classes("text-subtitle1 text-weight-medium")
|
||||
ui.label(f"Stored filename: {source.filename}").classes("text-body2")
|
||||
ui.label(f"Document id: {source.document_id}").classes("text-body2 vibe-text-muted")
|
||||
ui.button("Open source detail", on_click=lambda route=detail_path: ui.navigate.to(route), icon="open_in_new").props(
|
||||
"flat"
|
||||
)
|
||||
|
||||
# Format source records into read-model rows for the table renderer
|
||||
rows = [
|
||||
SourceTableRow(
|
||||
id=source.id,
|
||||
page_number=source.page_number,
|
||||
upload_name=source.upload_name,
|
||||
filename=source.filename,
|
||||
document_id=source.document_id,
|
||||
)
|
||||
for source in sources
|
||||
]
|
||||
render_sources_table(rows)
|
||||
|
||||
@ui.page("/sources/{source_id}")
|
||||
async def source_detail_page(source_id: str, request: Request, session_factory: SessionFactoryDep) -> None:
|
||||
apply_archival_theme()
|
||||
sources_service = TranscriptionService(session_factory=session_factory)
|
||||
render_navigation_header(current_path="/sources")
|
||||
|
||||
try:
|
||||
parsed_source_id = UUID(source_id)
|
||||
except ValueError:
|
||||
ui.label("Invalid source id").classes("text-h6 text-negative")
|
||||
ui.label("Invalid source id").classes("text-h6 text-red-800 p-4")
|
||||
return
|
||||
|
||||
try:
|
||||
source = await sources_service.read_source_detail(source_id=parsed_source_id)
|
||||
except TranscriptionNotFoundError:
|
||||
ui.label("Source not found").classes("text-h6 text-negative")
|
||||
ui.label("Source not found").classes("text-h6 text-red-800 p-4")
|
||||
return
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Load failed", operation="sources.read")
|
||||
return
|
||||
|
||||
back_path = _back_path_from_query(request.query_params)
|
||||
ui.label(f"Source {source.upload_name}").classes("text-h5 text-weight-medium")
|
||||
|
||||
with ui.row().classes("w-full items-center gap-2"):
|
||||
if back_path is not None:
|
||||
back_label = "Back to Document" if "document_id" in request.query_params else "Back to Job" if "job_id" in request.query_params else "Back to Sources"
|
||||
ui.button(back_label, on_click=lambda route=back_path: ui.navigate.to(route), icon="arrow_back")
|
||||
else:
|
||||
ui.button("Back to Sources", on_click=lambda: ui.navigate.to("/sources"), icon="arrow_back")
|
||||
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"):
|
||||
page_header(f"Source Page {source.page_number}: {source.upload_name}", subtitle=f"Source ID: {source.id}")
|
||||
|
||||
with ui.column().classes("w-full gap-3"):
|
||||
render_document_panzoom(source=source)
|
||||
if back_path is not None:
|
||||
back_label = (
|
||||
"Back to Document"
|
||||
if "document_id" in request.query_params
|
||||
else "Back to Job"
|
||||
if "job_id" in request.query_params
|
||||
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"
|
||||
)
|
||||
else:
|
||||
ui.button("Back to Sources", on_click=lambda: ui.navigate.to("/sources"), icon="arrow_back").props(
|
||||
"flat text-xs"
|
||||
)
|
||||
|
||||
with ui.column().classes("gap-1"):
|
||||
ui.label(f"Page number: {source.page_number}").classes("text-body2")
|
||||
ui.label(f"Upload name: {source.upload_name}").classes("text-body2")
|
||||
ui.label(f"Stored filename: {source.filename}").classes("text-body2")
|
||||
ui.label(f"Document id: {source.document_id}").classes("text-body2")
|
||||
ui.label(f"Uploaded: {source.date_uploaded.isoformat()}").classes("text-body2")
|
||||
ui.label(f"Revised: {source.date_revised.isoformat() if source.date_revised else 'not set'}").classes("text-body2")
|
||||
with ui.grid().classes("w-full grid-cols-12 gap-4"):
|
||||
with ui.column().classes("col-span-12 lg:col-span-7 gap-4"):
|
||||
with archival_card(title="Source Inspection Viewer", extra_classes="p-2"):
|
||||
render_document_panzoom(source=source)
|
||||
|
||||
ui.separator()
|
||||
ui.label("Transcription text").classes("text-subtitle1 text-weight-medium")
|
||||
ui.textarea(value=_source_transcription_text(source) or "").props("outlined autogrow readonly").classes("w-full")
|
||||
with ui.column().classes("col-span-12 lg:col-span-5 gap-4"):
|
||||
with archival_card(title="Source Metadata"):
|
||||
metadata_row("Page Number:", str(source.page_number))
|
||||
metadata_row("Upload Name:", source.upload_name)
|
||||
metadata_row("Stored Filename:", source.filename)
|
||||
metadata_row("Document ID:", str(source.document_id))
|
||||
metadata_row("Date Uploaded:", source.date_uploaded.isoformat())
|
||||
metadata_row(
|
||||
"Date Revised:",
|
||||
source.date_revised.isoformat() if source.date_revised else "Not revised",
|
||||
)
|
||||
|
||||
ui.label("Revision text").classes("text-subtitle1 text-weight-medium")
|
||||
revision_input = ui.textarea(label="Revision text", value=source.revised_text or "").props("outlined autogrow")
|
||||
revision_input.classes("w-full")
|
||||
with archival_card(title="Automated Raw Transcription"):
|
||||
ui.textarea(value=_source_transcription_text(source) or "").props("outlined autogrow readonly bg-white").classes(
|
||||
"w-full text-xs font-mono"
|
||||
)
|
||||
|
||||
async def save_revision() -> None:
|
||||
candidate = (revision_input.value or "").strip()
|
||||
if not candidate:
|
||||
ui.notify("Revision text is required.", type="warning")
|
||||
return
|
||||
with archival_card(title="Curated Human Transcription"):
|
||||
revision_input = (
|
||||
ui.textarea(value=source.revised_text or "")
|
||||
.props("outlined autogrow bg-white")
|
||||
.classes("w-full text-xs")
|
||||
)
|
||||
|
||||
try:
|
||||
await sources_service.upsert_revision_for_source(source_id=source.id, text=candidate)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Save failed", operation="sources.save_revision")
|
||||
return
|
||||
async def save_revision() -> None:
|
||||
candidate = (revision_input.value or "").strip()
|
||||
if not candidate:
|
||||
ui.notify("Revision text is required.", type="warning")
|
||||
return
|
||||
|
||||
ui.notify("Revision saved", type="positive")
|
||||
ui.navigate.to(request.url.path + _back_query(request.query_params))
|
||||
try:
|
||||
await sources_service.upsert_revision_for_source(source_id=source.id, text=candidate)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Save failed", operation="sources.save_revision")
|
||||
return
|
||||
|
||||
with ui.row().classes("w-full items-center gap-2"):
|
||||
ui.button("Save revision", on_click=save_revision, icon="save").props('unelevated color="primary"')
|
||||
ui.notify("Revision saved", type="positive")
|
||||
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.page("/documents/{document_id}/sources")
|
||||
async def document_sources_page(document_id: str) -> RedirectResponse:
|
||||
|
||||
Reference in New Issue
Block a user