V2 (new) sStep 4 complete. (untested, unreviewed, cross my fingers)

This commit is contained in:
Jim Lancaster
2026-08-01 18:28:00 -05:00
parent 51ac2d0b98
commit dfe6f121ff
8 changed files with 175 additions and 11 deletions
+7
View File
@@ -42,6 +42,13 @@ Use a fresh database. There will be no migrations, data conversion, legacy compa
- Test both service-owned sessions and caller-provided sessions so flush/commit behavior remains correct. - Test both service-owned sessions and caller-provided sessions so flush/commit behavior remains correct.
- Run the focused database and service tests, then the full suite with `uv run pytest`. - Run the focused database and service tests, then the full suite with `uv run pytest`.
### 4. Update the UI for the V2 schema
- Review the UI components and views that display document, job, person, and source data so they reference the V2 schema instead of V1 relationships.
- Update upload, detail, and listing screens to show the new person and source associations, revised-source fields, and the revised status values.
- Keep the UI behavior aligned with the updated service layer and ensure the existing UI tests continue to pass with the V2 data model.
- Consider the guidance in `docs/ui_style_guide.md` when making UI changes so the updated views remain consistent with the projects visual and interaction conventions.
## Done When ## Done When
- A fresh database is created directly from the V2 SQLModel metadata. - A fresh database is created directly from the V2 SQLModel metadata.
+107
View File
@@ -0,0 +1,107 @@
from nicegui import ui
# 1. Mature Dark Mode Setup
ui.dark_mode(True)
# Define a refined dark palette using expanded dictionary styling
theme_colors = {
'primary': '#6366f1',
'secondary': '#8b5cf6',
'accent': '#ec4899',
'dark': '#0f172a',
'dark_page': '#020617',
'positive': '#10b981',
'negative': '#ef4444',
}
ui.colors(**theme_colors)
# Optional: Add custom CSS for subtle noise overlays or kinetic typography
ui.add_css('''
.glass-card {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 1.5rem;
}
''')
# 2. Bento Grid Layout
with ui.element('div').classes('grid grid-cols-1 md:grid-cols-4 gap-6 w-full max-w-6xl mx-auto p-8'):
# Header spanning all columns
with ui.element('div').classes('col-span-1 md:col-span-4 mb-4'):
ui.label('Analytics Dashboard').classes('text-4xl font-extrabold tracking-tight text-white')
ui.label('AI-driven insights for Q3').classes('text-lg text-slate-400 mt-1')
# Large Feature Card (Glassmorphism + Functional Motion)
with ui.element('div').classes('glass-card col-span-1 md:col-span-2 p-6 transition-transform duration-300 hover:scale-[1.02]'):
ui.icon('monitoring', size='2rem').classes('text-primary mb-4')
ui.label('Revenue Prediction').classes('text-xl font-semibold text-slate-100')
ui.label('$45,231.00').classes('text-5xl font-bold text-white mt-2')
# Placeholder for an interactive EChart
ui.echart({
'xAxis': {
'type': 'category',
'data': [
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
],
},
'yAxis': {
'type': 'value',
},
'series': [
{
'data': [
120,
200,
150,
80,
70,
],
'type': 'bar',
'itemStyle': {
'color': '#6366f1',
},
},
],
}).classes('w-full h-48 mt-4')
# Smaller Metric Cards
metric_cards = [
{
'title': 'Active Users',
'value': '1,204',
'icon': 'group',
'color': 'text-secondary',
},
{
'title': 'Server Load',
'value': '34%',
'icon': 'memory',
'color': 'text-accent',
},
]
for card in metric_cards:
with ui.element('div').classes('glass-card col-span-1 p-6 flex flex-col justify-between transition-transform duration-300 hover:-translate-y-1'):
ui.icon(card['icon'], size='2rem').classes(card['color'])
ui.element('div').classes('flex-grow')
ui.label(card['value']).classes('text-4xl font-bold text-white mt-4')
ui.label(card['title']).classes('text-sm font-medium text-slate-400 uppercase tracking-wider')
# AI Assistant Module (Adaptive Interface)
with ui.element('div').classes('glass-card col-span-1 md:col-span-4 p-6 flex items-center gap-4'):
ui.icon('smart_toy', size='2rem').classes('text-positive animate-pulse')
with ui.element('div'):
ui.label('Ambient AI Suggestion').classes('text-sm font-bold text-positive uppercase tracking-wider')
ui.label('Based on current server load, scaling up instances in the EU-West region is recommended.').classes('text-slate-300')
ui.space()
ui.button('Apply Now', color='positive').classes('rounded-full px-6 py-2 shadow-lg shadow-positive/20')
ui.run(title='2026 UI Dashboard')
+50
View File
@@ -0,0 +1,50 @@
# UI Design Specification
## 1. Overview and Aesthetic Philosophy
This document outlines the design language for a high-density document transcription workflow application built in Python. The UI represents a deliberate blend of **Modern Modular Dashboard** design and an **Archival/Academic** aesthetic.
The interface balances modern structural trends with stylistic nods to historical preservation, ensuring maximum legibility, reduced eye strain during extended transcription sessions, and a clear presentation of complex relational data.
## 2. Key Structural Patterns
### 2.1. Modern Bento Grid Layout
The structure strictly adheres to the Bento Grid trend.
* The interface is divided into well-defined rectangular zones (e.g., image viewer, transcription editors, right-hand metadata column).
* This layout is highly functional for complex workflows, allowing users to cross-reference the original scanned document, raw AI output, human revision, and database relationships simultaneously without scrolling.
### 2.2. High-Density Flat Design (Flat 2.0)
The styling deliberately avoids heavy drop shadows or aggressive 3D glassmorphism.
* **Subtle Separation:** Relies on 1px borders and slight variations in background shade to separate functional zones.
* **Data Density:** Prioritizes fitting a large amount of structured data (processing metadata, related entities, tokens) into a compact space while remaining readable.
### 2.3. Dual-Typography System
The interface employs a classic typographic pairing standard in editorial or academic software:
* **Serif Headers:** Used for primary document titles. This lends a traditional, authoritative, and print-like feel appropriate for historical documents.
* **Sans-Serif UI/Body Text:** Functional UI elements, buttons, and transcription input fields use a clean, modern sans-serif font for maximum utility.
## 3. The "Archival" Color Palette
The color palette is strictly utilitarian: prioritizing glare elimination, minimizing eye strain for fading texts, and establishing hierarchy without distracting from the documents.
### 3.1 Palette Philosophy
* **The "Canvas" (Backgrounds):** A warm off-white/cream mimicking standard archival folder stock to provide a low-contrast environment. Card surfaces use a slightly elevated manila/parchment hue for visual separation.
* **The "Ink" (Typography):** Deep charcoal or slate (aged iron-gall ink) instead of pure black to prevent visual vibration. Secondary text uses a muted grey-brown.
* **The "Accents" (Interactive Elements):** A sophisticated forest green for primary actions/tabs, and warm earthy sepia/tan for badges and highlights.
* **The "Dark Room" (Image Viewer):** A very dark, matte slate isolates the scanned original. This high contrast eliminates screen glare, ensuring faded handwriting remains the absolute focal point.
### 3.2 Exact Color Specifications
| UI Role | Color Name | Hex Code | Tailwind Equivalent (Approx) |
| :--- | :--- | :--- | :--- |
| App Background | Archival Cream | `#FAF9F6` | `stone-50` |
| Card Surface | Folder Stock | `#F4F0E6` | `stone-100` |
| Image Viewer | Matte Slate | `#2B2D2C` | `zinc-800` |
| Primary Text | Iron Ink | `#333333` | `neutral-800` |
| Secondary Text | Faded Type | `#6B6A65` | `stone-500` |
| Primary Accent | Library Green | `#2D5A4C` | `emerald-800` |
| Secondary Accent | Aged Sepia | `#E2C7A8` | `orange-200` |
## 4. Implementation Guidelines
* When generating layout code, utilize UI framework grid utilities to construct the Bento Grid.
* Map the custom hex codes to the application's global theme variables to ensure consistency across all components.
* Prioritize static borders and background colors over hover effects or transition animations to maintain the academic utility of the interface.
@@ -49,11 +49,11 @@ def _render_source_section(source: Source) -> None:
def _render_revision_section(revision: Source | None) -> None: def _render_revision_section(revision: Source | None) -> None:
with ui.card().classes("w-full q-pa-md vibe-card"): with ui.card().classes("w-full q-pa-md vibe-card"):
ui.label("Revision").classes("text-subtitle1 text-weight-medium") ui.label("Source revision").classes("text-subtitle1 text-weight-medium")
ui.separator().classes("q-my-sm") ui.separator().classes("q-my-sm")
if revision is None: if revision is None:
ui.label("No revision exists for this source.").classes("text-body2 vibe-text-muted") ui.label("No source revision exists for this source.").classes("text-body2 vibe-text-muted")
return return
render_revision_row(revision=revision, initially_expanded=True) render_revision_row(revision=revision, initially_expanded=True)
@@ -39,14 +39,14 @@ def render_page_content(
ui.label(raw_text).classes("transcription-placeholder__text") ui.label(raw_text).classes("transcription-placeholder__text")
with ui.element("section").classes("transcription-placeholder__section"): with ui.element("section").classes("transcription-placeholder__section"):
ui.label("Human revision").classes("transcription-placeholder__title") ui.label("Human-reviewed text").classes("transcription-placeholder__title")
ui.textarea(value=revised_text).props("outlined autogrow").classes("w-full") 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("aside").props('aria-label="Document context"').classes("page-content__sidebar"):
with ui.element("section").classes("page-content__sidebar-section"): with ui.element("section").classes("page-content__sidebar-section"):
ui.label("People").classes("page-content__sidebar-title") ui.label("People").classes("page-content__sidebar-title")
ui.label("Author · Placeholder Person") ui.label("Author · Placeholder person")
ui.label("Recipient · Placeholder Person") ui.label("Recipient · Placeholder person")
with ui.element("section").classes("page-content__sidebar-section"): with ui.element("section").classes("page-content__sidebar-section"):
ui.label("AI processing").classes("page-content__sidebar-title") ui.label("AI processing").classes("page-content__sidebar-title")
@@ -57,7 +57,7 @@ def render_revision_row(
if revision is None: if revision is None:
return None return None
header = "Revision | User-authored" header = "Source revision | User-authored"
caption = _format_created_at(revision.date_revised or revision.date_uploaded) caption = _format_created_at(revision.date_revised or revision.date_uploaded)
expansion = ui.expansion(value=initially_expanded, group="group").classes(f"{classes} rounded-borders vibe-card") expansion = ui.expansion(value=initially_expanded, group="group").classes(f"{classes} rounded-borders vibe-card")
@@ -70,7 +70,7 @@ def render_revision_row(
if on_delete is not None: if on_delete is not None:
with ui.dialog() as delete_dialog, ui.card().classes("q-pa-md"): with ui.dialog() as delete_dialog, ui.card().classes("q-pa-md"):
ui.label("Delete this transcript revision?").classes("text-body1") ui.label("Delete this source revision?").classes("text-body1")
with ui.row().classes("w-full justify-end q-gutter-sm"): with ui.row().classes("w-full justify-end q-gutter-sm"):
ui.button("Cancel", on_click=lambda: delete_dialog.submit(False)).props("flat") ui.button("Cancel", on_click=lambda: delete_dialog.submit(False)).props("flat")
ui.button("Delete", on_click=lambda: delete_dialog.submit(True)).props( ui.button("Delete", on_click=lambda: delete_dialog.submit(True)).props(
+1 -1
View File
@@ -128,7 +128,7 @@ def register_page() -> None: # noqa: PLR0915
).props('unelevated color="primary"') ).props('unelevated color="primary"')
if current_revision_text is None: if current_revision_text is None:
ui.label("No revision exists for this source.").classes("text-body2 vibe-text-muted") ui.label("No source revision exists for this source.").classes("text-body2 vibe-text-muted")
return return
render_revision_row( render_revision_row(
+3 -3
View File
@@ -48,8 +48,8 @@ class TestPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Original Transcription" in response.text assert "Original Transcription" in response.text
assert "detail.pdf" in response.text assert "detail.pdf" in response.text
assert "Revision" in response.text assert "Source revision" in response.text
assert "Revision" in response.text assert "Source revision" in response.text
assert "hello" in response.text assert "hello" in response.text
assert "original text" in response.text assert "original text" in response.text
assert "Document preview" in response.text assert "Document preview" in response.text
@@ -87,7 +87,7 @@ class TestPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Revision Editor" in response.text assert "Revision Editor" in response.text
assert "Create revision" in response.text assert "Create revision" in response.text
assert "No revision exists for this source." in response.text assert "No source revision exists for this source." in response.text
def test_job_detail_page_shows_update_action_for_existing_revision(self, app_client, seed_job): def test_job_detail_page_shows_update_action_for_existing_revision(self, app_client, seed_job):
"""GET /ui/jobs/{job_id} renders revision editor with update action when revision exists.""" """GET /ui/jobs/{job_id} renders revision editor with update action when revision exists."""