generated from john/python-template
V2 (new) sStep 4 complete. (untested, unreviewed, cross my fingers)
This commit is contained in:
@@ -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.
|
||||
- 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 project’s visual and interaction conventions.
|
||||
|
||||
## Done When
|
||||
|
||||
- A fresh database is created directly from the V2 SQLModel metadata.
|
||||
|
||||
@@ -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')
|
||||
@@ -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:
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
render_revision_row(revision=revision, initially_expanded=True)
|
||||
|
||||
@@ -39,14 +39,14 @@ def render_page_content(
|
||||
ui.label(raw_text).classes("transcription-placeholder__text")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
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")
|
||||
|
||||
@@ -57,7 +57,7 @@ def render_revision_row(
|
||||
if revision is None:
|
||||
return None
|
||||
|
||||
header = "Revision | User-authored"
|
||||
header = "Source revision | User-authored"
|
||||
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")
|
||||
@@ -70,7 +70,7 @@ def render_revision_row(
|
||||
|
||||
if on_delete is not None:
|
||||
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"):
|
||||
ui.button("Cancel", on_click=lambda: delete_dialog.submit(False)).props("flat")
|
||||
ui.button("Delete", on_click=lambda: delete_dialog.submit(True)).props(
|
||||
|
||||
@@ -128,7 +128,7 @@ def register_page() -> None: # noqa: PLR0915
|
||||
).props('unelevated color="primary"')
|
||||
|
||||
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
|
||||
|
||||
render_revision_row(
|
||||
|
||||
@@ -48,8 +48,8 @@ class TestPageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "Original Transcription" in response.text
|
||||
assert "detail.pdf" in response.text
|
||||
assert "Revision" in response.text
|
||||
assert "Revision" in response.text
|
||||
assert "Source revision" in response.text
|
||||
assert "Source revision" in response.text
|
||||
assert "hello" in response.text
|
||||
assert "original text" in response.text
|
||||
assert "Document preview" in response.text
|
||||
@@ -87,7 +87,7 @@ class TestPageRendering:
|
||||
assert response.status_code == 200
|
||||
assert "Revision Editor" 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):
|
||||
"""GET /ui/jobs/{job_id} renders revision editor with update action when revision exists."""
|
||||
|
||||
Reference in New Issue
Block a user