claude-sonnet-5 review: Phase 4 (by gpt-5.3-codex)
Quality Gate / gate (push) Failing after 11s

This commit is contained in:
Jim Lancaster
2026-08-20 16:04:37 -05:00
parent 450d33d507
commit 8b08478c9d
19 changed files with 512 additions and 210 deletions
+8 -10
View File
@@ -72,19 +72,17 @@ class DocumentPeopleResponse(ApiModel):
def _document_type_to_read(item: DocumentType) -> DocumentTypeRead:
return DocumentTypeRead(
id=item.id,
label=item.label,
is_active=item.is_active,
)
item_id, label, is_active = _registry_read_values(item)
return DocumentTypeRead(id=item_id, label=label, is_active=is_active)
def _person_role_to_read(item: PersonRole) -> PersonRoleRead:
return PersonRoleRead(
id=item.id,
label=item.label,
is_active=item.is_active,
)
item_id, label, is_active = _registry_read_values(item)
return PersonRoleRead(id=item_id, label=label, is_active=is_active)
def _registry_read_values(item: DocumentType | PersonRole) -> tuple[UUID, str, bool]:
return item.id, item.label, item.is_active
def _document_person_to_read(item: DocumentPerson) -> DocumentPersonRead:
+4 -11
View File
@@ -26,6 +26,7 @@ from ..errors import AppError
from ..errors import ErrorCategory
from .base import ServiceBase
from .registry import RegistryService
from .registry import RegistrySummary
from .source_media import lookup_source_mime_type
from .source_media import supported_source_formats
@@ -71,15 +72,7 @@ class DocumentTypeRegistry(RegistryService[DocumentType]):
return col(Document.document_type_id)
@dataclass(frozen=True, slots=True)
class DocumentTypeSummary:
"""Settings read model for a Document Type and its usage count."""
id: UUID
label: str
is_active: bool
is_built_in: bool
document_count: int
type DocumentTypeSummary = RegistrySummary
@dataclass(frozen=True, slots=True)
@@ -377,12 +370,12 @@ class DocumentService(ServiceBase):
"""List Document Types alphabetically with current usage counts."""
rows = await self._document_types.list_entries_with_counts(session=session)
return [
DocumentTypeSummary(
RegistrySummary(
id=document_type.id,
label=document_type.label,
is_active=document_type.is_active,
is_built_in=document_type.semantic_key is not None,
document_count=document_count,
reference_count=document_count,
)
for document_type, document_count in rows
]
@@ -52,6 +52,36 @@ async def write_media_bytes(
return stored_path
async def persist_named_media(
*,
root: Path,
filename: str,
file_bytes: bytes,
error: type[AppError],
failure_message: str,
failure_suggestion: str,
log_label: str,
namespace: Path | str | None = None,
filename_stem: str | None = None,
preserve_original_name: bool = False,
) -> Path:
"""Resolve a target directory/name and persist media bytes safely."""
target_dir = root if namespace is None else root / Path(namespace)
stored_name = Path(filename).name if preserve_original_name else build_stored_filename(
filename=filename,
filename_stem=filename_stem,
)
return await write_media_bytes(
target_dir=target_dir,
stored_name=stored_name,
file_bytes=file_bytes,
error=error,
failure_message=failure_message,
failure_suggestion=failure_suggestion,
log_label=log_label,
)
def _write(stored_path: Path, file_bytes: bytes) -> None:
stored_path.parent.mkdir(parents=True, exist_ok=True)
stored_path.write_bytes(file_bytes)
+9 -16
View File
@@ -28,9 +28,9 @@ from ..db.models import PersonRole
from ..errors import AppError
from ..errors import ErrorCategory
from .base import ServiceBase
from .media_storage import build_stored_filename
from .media_storage import write_media_bytes
from .media_storage import persist_named_media
from .registry import RegistryService
from .registry import RegistrySummary
logger = logging.getLogger(__name__)
@@ -83,15 +83,7 @@ def normalize_family_search_id(value: str | None) -> str | None:
return normalized
@dataclass(frozen=True, slots=True)
class PersonRoleSummary:
"""Settings read model for a Person Role and its usage count."""
id: UUID
label: str
is_active: bool
is_built_in: bool
link_count: int
type PersonRoleSummary = RegistrySummary
@dataclass(frozen=True, slots=True)
@@ -241,12 +233,12 @@ class PeopleService(ServiceBase):
"""List Person Roles alphabetically with current link counts."""
rows = await self._person_roles.list_entries_with_counts(session=session)
return [
PersonRoleSummary(
RegistrySummary(
id=role.id,
label=role.label,
is_active=role.is_active,
is_built_in=role.semantic_key is not None,
link_count=link_count,
reference_count=link_count,
)
for role, link_count in rows
]
@@ -543,9 +535,10 @@ async def store_person_portrait(
)
runtime_settings = settings or get_settings()
return await write_media_bytes(
target_dir=runtime_settings.upload_dir / "persons" / str(person_id),
stored_name=build_stored_filename(filename=filename),
return await persist_named_media(
root=runtime_settings.upload_dir,
namespace=Path("persons") / str(person_id),
filename=filename,
file_bytes=file_bytes,
error=PersonMediaError,
failure_message="Failed to persist Person portrait media",
+22
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
from abc import abstractmethod
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from typing import Protocol
from uuid import UUID
@@ -43,6 +44,27 @@ class RegistryEntry(Protocol):
def __init__(self, /, **data: Any) -> None: ...
@dataclass(frozen=True, slots=True)
class RegistrySummary:
"""Shared settings read model for label-keyed registries and usage counts."""
id: UUID
label: str
is_active: bool
is_built_in: bool
reference_count: int
@property
def document_count(self) -> int:
"""Backward-compatible alias for document-type settings consumers."""
return self.reference_count
@property
def link_count(self) -> int:
"""Backward-compatible alias for person-role settings consumers."""
return self.reference_count
class RegistryService[ModelT: RegistryEntry](ServiceBase):
"""Generic create/read/update/delete behavior for a registry table.
+6 -5
View File
@@ -24,8 +24,7 @@ from ..db.models import Source
from ..db.session import SessionFactory
from ..db.session import session_scope
from .errors import TranscriptionError
from .media_storage import build_stored_filename
from .media_storage import write_media_bytes
from .media_storage import persist_named_media
from .normalization import normalize_orientation_async
from .sources import build_prompt_execution
from .sources import source_mime_type
@@ -375,9 +374,11 @@ async def store_source_file(
file_bytes = normalized.content
upload_dir = runtime_settings.upload_dir
stored_path = await write_media_bytes(
target_dir=upload_dir if relative_directory is None else upload_dir / relative_directory,
stored_name=build_stored_filename(filename=filename, filename_stem=filename_stem),
stored_path = await persist_named_media(
root=upload_dir,
namespace=relative_directory,
filename=filename,
filename_stem=filename_stem,
file_bytes=file_bytes,
error=SourceStorageError,
failure_message="Failed to persist Source file",
@@ -2,6 +2,11 @@
from __future__ import annotations
from collections.abc import Awaitable
from collections.abc import Callable
from dataclasses import dataclass
from typing import TypeVar
from nicegui import ui
from transcription.errors import AppError
@@ -9,6 +14,33 @@ from transcription.errors import ErrorCategory
from transcription.errors import canonical_error_category
from transcription.errors import classify_unexpected_error
T = TypeVar("T")
@dataclass(frozen=True, slots=True)
class UiActionOutcome[T]:
"""Result of a UI action wrapper."""
ok: bool
value: T | None = None
async def run_ui_action(
*,
operation: str,
title: str,
action: Callable[[], Awaitable[T]],
passthrough: tuple[type[Exception], ...] = (),
) -> UiActionOutcome[T]:
"""Run an async UI action and render consistent errors."""
try:
return UiActionOutcome(ok=True, value=await action())
except Exception as exc:
if isinstance(exc, passthrough):
raise
show_error(exc, title=title, operation=operation)
return UiActionOutcome(ok=False, value=None)
def to_app_error(exc: Exception, *, operation: str) -> AppError:
"""Normalize any exception for consistent UI display."""
+5 -4
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from transcription.config import Settings
from transcription.config import get_settings
from transcription.errors import AppError
from transcription.services.media_storage import write_media_bytes
from transcription.services.media_storage import persist_named_media
HOME_PAGE_MARKDOWN_NAME = "homepage.md"
SUPPORTED_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"}
@@ -68,9 +68,10 @@ async def store_homepage_image(
msg = "Homepage image filename is required"
raise ValueError(msg)
return await write_media_bytes(
target_dir=homepage_dir(settings),
stored_name=safe_name,
return await persist_named_media(
root=homepage_dir(settings),
filename=safe_name,
preserve_original_name=True,
file_bytes=file_bytes,
error=HomepageStorageError,
failure_message="Failed to persist homepage image",
+34 -22
View File
@@ -26,6 +26,7 @@ from transcription.ui.components.confirm_delete import render_delete_actions
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
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 run_ui_action
from transcription.ui.components.error_presenter import show_error
from transcription.ui.components.formatters import compact_date
from transcription.ui.components.formatters import parse_iso_date
@@ -77,8 +78,13 @@ def register_page() -> None: # noqa: PLR0915
requested_person_id = parse_uuid(request.query_params.get("person_id"))
staged_links: list[StagedLinkedPerson] = []
if requested_person_id is not None and any(person.id == requested_person_id for person in people):
try:
author_role = await people_service.read_person_role_by_semantic_key(AUTHOR_ROLE_SEMANTIC_KEY)
author_role_outcome = await run_ui_action(
operation="documents.create.preselect",
title="Author role unavailable",
action=lambda: people_service.read_person_role_by_semantic_key(AUTHOR_ROLE_SEMANTIC_KEY),
)
if author_role_outcome.ok and author_role_outcome.value is not None:
author_role = author_role_outcome.value
if author_role.is_active:
staged_links.append(StagedLinkedPerson(person_id=requested_person_id, role_id=author_role.id))
else:
@@ -86,8 +92,6 @@ def register_page() -> None: # noqa: PLR0915
"The Author role is inactive, so the Person could not be preselected.",
type="warning",
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Author role unavailable", operation="documents.create.preselect")
elif request.query_params.get("person_id"):
ui.notify("The requested person could not be preselected.", type="warning")
linked_people = LinkedPeopleEditor(
@@ -127,16 +131,19 @@ def register_page() -> None: # noqa: PLR0915
archive_identifier=(form.archive.value or "").strip() or None,
)
try:
created = await create_document_with_people(
created_outcome = await run_ui_action(
operation="documents.create",
title="Create failed",
action=lambda: create_document_with_people(
document=candidate,
links=linked_people.values(),
documents=document_service,
people=people_service,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Create failed", operation="documents.create")
),
)
if not created_outcome.ok or created_outcome.value is None:
return
created = created_outcome.value
ui.notify("Document created", type="positive")
if return_to == "jobs_new":
@@ -162,15 +169,18 @@ def register_page() -> None: # noqa: PLR0915
icon="note_add",
).classes("ui-btn-primary")
try:
documents = sorted(
await document_service.list_documents(),
key=lambda item: item.created_at,
reverse=True,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Load failed", operation="documents.list")
documents_outcome = await run_ui_action(
operation="documents.list",
title="Load failed",
action=document_service.list_documents,
)
if not documents_outcome.ok:
return
documents = sorted(
documents_outcome.value or (),
key=lambda item: item.created_at,
reverse=True,
)
rows = [
DocumentTableRow(
@@ -349,15 +359,17 @@ def register_page() -> None: # noqa: PLR0915
updated_at=document.updated_at,
)
try:
await update_document_with_people(
save_outcome = await run_ui_action(
operation="documents.edit.save",
title="Save failed",
action=lambda: update_document_with_people(
document=candidate,
links=linked_people.values(),
documents=document_service,
people=people_service,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Save failed", operation="documents.edit.save")
),
)
if not save_outcome.ok:
return
ui.notify("Document updated", type="positive")
+43 -26
View File
@@ -28,6 +28,7 @@ from transcription.ui.components.confirm_delete import render_delete_actions
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
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 run_ui_action
from transcription.ui.components.error_presenter import show_error
from transcription.ui.components.formatters import parse_uuid
from transcription.ui.components.guards import parsed_record_id
@@ -98,15 +99,18 @@ def register_page() -> None: # noqa: PLR0915
)
requested_source_id = parse_uuid(request.query_params.get("source_id"))
try:
locked_source = (
await sources_service.read_source_detail(requested_source_id)
locked_source_outcome = await run_ui_action(
operation="jobs.retranscribe.load",
title="Source unavailable",
action=(
(lambda: sources_service.read_source_detail(requested_source_id))
if requested_source_id is not None
else None
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Source unavailable", operation="jobs.retranscribe.load")
else _none_async
),
)
if not locked_source_outcome.ok:
return
locked_source = locked_source_outcome.value
documents = await documents_service.list_documents()
if not documents:
_render_no_documents_card()
@@ -156,21 +160,24 @@ def register_page() -> None: # noqa: PLR0915
async def submit_create() -> None:
if locked_source is not None:
try:
services = ServiceBundle(
documents=documents_service,
jobs=JobService(session_factory=session_factory, settings=settings),
sources=SourceService(session_factory=session_factory, settings=settings),
)
result_job = await create_source_retranscription_job(
services = ServiceBundle(
documents=documents_service,
jobs=JobService(session_factory=session_factory, settings=settings),
sources=SourceService(session_factory=session_factory, settings=settings),
)
result_outcome = await run_ui_action(
operation="jobs.retranscribe",
title="Create job failed",
action=lambda: create_source_retranscription_job(
source_id=locked_source.id,
model=str(model_input.value),
services=services,
settings=settings,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Create job failed", operation="jobs.retranscribe")
),
)
if not result_outcome.ok or result_outcome.value is None:
return
result_job = result_outcome.value
resolve_worker_notifier(request.app.state).notify()
ui.notify(f"Created retranscription job {result_job.id}", type="positive")
ui.navigate.to(f"/jobs/{result_job.id}")
@@ -189,17 +196,20 @@ def register_page() -> None: # noqa: PLR0915
ui.notify("Selected document id is invalid.", type="warning")
return
try:
result = await create_job_for_document(
create_outcome = await run_ui_action(
operation="jobs.create",
title="Create job failed",
action=lambda: create_job_for_document(
document_id=document_id,
source_files=uploaded_files,
provider=(provider_input.value or None),
model=(model_input.value or None),
session_factory=session_factory,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Create job failed", operation="jobs.create")
),
)
if not create_outcome.ok or create_outcome.value is None:
return
result = create_outcome.value
resolve_worker_notifier(request.app.state).notify()
ui.notify(f"Created job {result.job_id}", type="positive")
@@ -250,12 +260,15 @@ def register_page() -> None: # noqa: PLR0915
timer.cancel()
timer_holder[0] = None
try:
current_job[0] = await jobs_service.read_job(job_id=parsed_job_id)
except Exception as exc: # noqa: BLE001
refresh_outcome = await run_ui_action(
operation="jobs.detail.refresh",
title="Auto-refresh failed",
action=lambda: jobs_service.read_job(job_id=parsed_job_id),
)
if not refresh_outcome.ok or refresh_outcome.value is None:
stop_refresh()
show_error(exc, title="Auto-refresh failed", operation="jobs.detail.refresh")
return
current_job[0] = refresh_outcome.value
render_detail.refresh()
if current_job[0].status not in {JobStatus.QUEUED, JobStatus.PROCESSING}:
@@ -567,3 +580,7 @@ def _render_job_document_links(job: Job) -> None:
def _latest_prompt_name(job: Job) -> str | None:
return job.prompt_name
async def _none_async() -> None:
return None
+25 -16
View File
@@ -21,6 +21,7 @@ from transcription.ui.components.app_shell import render_navigation_header
from transcription.ui.components.cards import archival_card
from transcription.ui.components.confirm_delete import render_delete_actions
from transcription.ui.components.data_display import metadata_row
from transcription.ui.components.error_presenter import run_ui_action
from transcription.ui.components.error_presenter import show_error
from transcription.ui.components.formatters import compact_date
from transcription.ui.components.formatters import family_search_url
@@ -77,15 +78,18 @@ def register_page() -> None: # noqa: PLR0915
icon="person_add",
).classes("ui-btn-primary")
try:
people = sorted(
await people_service.list_people(),
key=lambda item: item.created_at,
reverse=True,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Load failed", operation="people.list")
people_outcome = await run_ui_action(
operation="people.list",
title="Load failed",
action=people_service.list_people,
)
if not people_outcome.ok:
return
people = sorted(
people_outcome.value or (),
key=lambda item: item.created_at,
reverse=True,
)
rows = [
PersonTableRow(
@@ -139,11 +143,14 @@ def register_page() -> None: # noqa: PLR0915
family_search_id=(form.family_search_id.value or "").strip() or None,
)
try:
created = await people_service.create_person(candidate)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Create failed", operation="people.create")
create_outcome = await run_ui_action(
operation="people.create",
title="Create failed",
action=lambda: people_service.create_person(candidate),
)
if not create_outcome.ok or create_outcome.value is None:
return
created = create_outcome.value
ui.notify("Person created", type="positive")
ui.navigate.to(f"/people/{created.id}")
@@ -256,10 +263,12 @@ def register_page() -> None: # noqa: PLR0915
updated_at=person.updated_at,
)
try:
await people_service.update_person(candidate)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Save failed", operation="people.edit.save")
save_outcome = await run_ui_action(
operation="people.edit.save",
title="Save failed",
action=lambda: people_service.update_person(candidate),
)
if not save_outcome.ok:
return
ui.notify("Person updated", type="positive")
+97 -55
View File
@@ -13,7 +13,7 @@ from transcription.services.people import PeopleService
from transcription.services.prompts import PromptStore
from transcription.ui.components.app_shell import render_navigation_header
from transcription.ui.components.cards import archival_card
from transcription.ui.components.error_presenter import show_error
from transcription.ui.components.error_presenter import run_ui_action
from transcription.ui.components.primitives import destructive_button
from transcription.ui.components.primitives import render_empty_state
from transcription.ui.components.primitives import section_header_row
@@ -45,11 +45,14 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
ui.label("Types are listed alphabetically. Select one row to edit or delete it.").classes(
"text-xs ui-text-muted mb-3"
)
try:
document_types = await documents.list_document_type_summaries()
except Exception as exc: # noqa: BLE001
show_error(exc, title="Document Types unavailable", operation="settings.types.list")
document_types_outcome = await run_ui_action(
operation="settings.types.list",
title="Document Types unavailable",
action=documents.list_document_type_summaries,
)
if not document_types_outcome.ok:
return
document_types = document_types_outcome.value or ()
if not document_types:
render_empty_state("No Document Types are configured.", extra_classes="mt-3")
@@ -75,17 +78,22 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
label: str,
is_active: bool,
) -> bool:
try:
async def _save_type() -> None:
if item_id is None:
await documents.create_document_type(label=label, is_active=is_active)
else:
await documents.update_document_type(
item_id,
label=label,
is_active=is_active,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Document Type save failed", operation="settings.types.save")
return
await documents.update_document_type(
item_id,
label=label,
is_active=is_active,
)
save_outcome = await run_ui_action(
operation="settings.types.save",
title="Document Type save failed",
action=_save_type,
)
if not save_outcome.ok:
return False
ui.notify("Document Type saved", type="positive")
render_document_types.refresh()
@@ -131,10 +139,12 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
if selected is None:
ui.notify("Select one Document Type to delete.", type="warning")
return
try:
await documents.delete_document_type(UUID(str(selected["id"])))
except Exception as exc: # noqa: BLE001
show_error(exc, title="Document Type deletion failed", operation="settings.types.delete")
delete_outcome = await run_ui_action(
operation="settings.types.delete",
title="Document Type deletion failed",
action=lambda: documents.delete_document_type(UUID(str(selected["id"]))),
)
if not delete_outcome.ok:
return
ui.notify("Document Type deleted", type="positive")
render_document_types.refresh()
@@ -153,11 +163,14 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
"Roles are listed alphabetically. Built-ins cannot be deleted; "
"referenced custom roles must be deactivated."
).classes("text-xs ui-text-muted mb-3")
try:
roles = await people.list_person_role_summaries()
except Exception as exc: # noqa: BLE001
show_error(exc, title="Person Roles unavailable", operation="settings.roles.list")
roles_outcome = await run_ui_action(
operation="settings.roles.list",
title="Person Roles unavailable",
action=people.list_person_role_summaries,
)
if not roles_outcome.ok:
return
roles = roles_outcome.value or ()
if not roles:
render_empty_state("No Person Roles are configured.", extra_classes="mt-3")
rows = [
@@ -182,13 +195,18 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
label: str,
is_active: bool,
) -> bool:
try:
async def _save_role() -> None:
if item_id is None:
await people.create_person_role(label=label, is_active=is_active)
else:
await people.update_person_role(item_id, label=label, is_active=is_active)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Person Role save failed", operation="settings.roles.save")
return
await people.update_person_role(item_id, label=label, is_active=is_active)
save_outcome = await run_ui_action(
operation="settings.roles.save",
title="Person Role save failed",
action=_save_role,
)
if not save_outcome.ok:
return False
ui.notify("Person Role saved", type="positive")
render_person_roles.refresh()
@@ -234,10 +252,12 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
if selected is None:
ui.notify("Select one Person Role to delete.", type="warning")
return
try:
await people.delete_person_role(UUID(str(selected["id"])))
except Exception as exc: # noqa: BLE001
show_error(exc, title="Person Role deletion failed", operation="settings.roles.delete")
delete_outcome = await run_ui_action(
operation="settings.roles.delete",
title="Person Role deletion failed",
action=lambda: people.delete_person_role(UUID(str(selected["id"]))),
)
if not delete_outcome.ok:
return
ui.notify("Person Role deleted", type="positive")
render_person_roles.refresh()
@@ -250,25 +270,31 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
destructive_button("Delete", icon="delete", on_click=delete_selected_role)
@ui.refreshable
def render_prompts() -> None:
async def render_prompts() -> None:
with archival_card("Prompts"):
ui.label("Only existing Markdown prompts can be edited. Changes affect future Jobs only.").classes(
"text-xs ui-text-muted mb-3"
)
try:
summaries = prompts.list_prompts()
except Exception as exc: # noqa: BLE001
show_error(exc, title="Prompts unavailable", operation="settings.prompts.list")
summaries_outcome = await run_ui_action(
operation="settings.prompts.list",
title="Prompts unavailable",
action=lambda: _list_prompts(prompts),
)
if not summaries_outcome.ok:
return
summaries = summaries_outcome.value or ()
if not summaries:
render_empty_state("No editable Markdown prompts were found.")
for summary in summaries:
try:
content = prompts.read_prompt(summary.name)
except Exception as exc: # noqa: BLE001
show_error(exc, title=f"{summary.name} unavailable", operation="settings.prompts.read")
read_outcome = await run_ui_action(
operation="settings.prompts.read",
title=f"{summary.name} unavailable",
action=lambda name=summary.name: _read_prompt(prompts, name),
)
if not read_outcome.ok:
continue
content = read_outcome.value or ""
with ui.column().classes("w-full gap-2 py-3 ui-header-divider"):
with section_header_row():
@@ -283,28 +309,28 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
.classes("w-full")
)
def save_prompt(
async def save_prompt(
*,
name: str = summary.name,
field: Any = editor,
) -> None:
try:
prompts.write_prompt(name, str(field.value or ""))
except Exception as exc: # noqa: BLE001
show_error(exc, title="Prompt save failed", operation="settings.prompts.write")
save_outcome = await run_ui_action(
operation="settings.prompts.write",
title="Prompt save failed",
action=lambda: _write_prompt(prompts, name, str(field.value or "")),
)
if not save_outcome.ok:
return
ui.notify(f"{name} saved for future Jobs", type="positive")
render_prompts.refresh()
def recover_prompt(*, name: str = summary.name) -> None:
try:
prompts.recover_prompt(name)
except Exception as exc: # noqa: BLE001
show_error(
exc,
title="Prompt recovery failed",
operation="settings.prompts.recover",
)
async def recover_prompt(*, name: str = summary.name) -> None:
recover_outcome = await run_ui_action(
operation="settings.prompts.recover",
title="Prompt recovery failed",
action=lambda: _recover_prompt(prompts, name),
)
if not recover_outcome.ok:
return
ui.notify(f"{name} restored from its previous version", type="positive")
render_prompts.refresh()
@@ -321,7 +347,7 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
await render_document_types()
await render_person_roles()
render_prompts()
await render_prompts()
def _selected_table_row(table: Any) -> dict[str, Any] | None:
@@ -329,3 +355,19 @@ def _selected_table_row(table: Any) -> dict[str, Any] | None:
if len(selected) != 1:
return None
return selected[0]
async def _list_prompts(prompts: PromptStore) -> tuple[Any, ...]:
return prompts.list_prompts()
async def _read_prompt(prompts: PromptStore, name: str) -> str:
return prompts.read_prompt(name)
async def _write_prompt(prompts: PromptStore, name: str, content: str) -> None:
prompts.write_prompt(name, content)
async def _recover_prompt(prompts: PromptStore, name: str) -> None:
prompts.recover_prompt(name)
+22 -13
View File
@@ -25,6 +25,7 @@ from transcription.ui.components.confirm_delete import render_delete_actions
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
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 run_ui_action
from transcription.ui.components.error_presenter import show_error
from transcription.ui.components.formatters import parse_uuid
from transcription.ui.components.guards import parsed_record_id
@@ -62,11 +63,14 @@ def register_page() -> None: # noqa: PLR0915
elif parsed_job_id is not None:
header_title = "Sources for Job"
try:
sources = await sources_service.list_sources_detail(document_id=parsed_doc_id, job_id=parsed_job_id)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Load failed", operation="sources.list")
sources_outcome = await run_ui_action(
operation="sources.list",
title="Load failed",
action=lambda: sources_service.list_sources_detail(document_id=parsed_doc_id, job_id=parsed_job_id),
)
if not sources_outcome.ok:
return
sources = sources_outcome.value or ()
render_navigation_header(current_path="/sources")
@@ -424,11 +428,14 @@ def _transport_display(latest_attempt: LatestExecutionAttempt) -> dict[str, obje
async def _download_evidence(*, source_id: UUID, evidence_service: EvidenceService) -> None:
try:
payload = await evidence_service.build_evidence_export(source_id=source_id)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Export failed", operation="sources.evidence_export")
payload_outcome = await run_ui_action(
operation="sources.evidence_export",
title="Export failed",
action=lambda: evidence_service.build_evidence_export(source_id=source_id),
)
if not payload_outcome.ok or payload_outcome.value is None:
return
payload = payload_outcome.value
ui.download(
json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False).encode("utf-8"),
filename=f"source-{source_id}-evidence-v1.json",
@@ -594,13 +601,15 @@ def _render_machine_candidates(
)
async def promote(candidate_id: UUID = attempt.id) -> None:
try:
await evidence_service.promote_machine_attempt(
promote_outcome = await run_ui_action(
operation="sources.candidate.promote",
title="Promotion failed",
action=lambda: evidence_service.promote_machine_attempt(
source_id=source.id,
execution_attempt_id=candidate_id,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Promotion failed", operation="sources.candidate.promote")
),
)
if not promote_outcome.ok:
return
ui.notify("Preferred machine transcription updated", type="positive")
ui.navigate.to(f"/sources/{source.id}")