|
1 | 1 | from fasthtml.common import * |
2 | 2 |
|
3 | | -def Layout(title, content): |
4 | | - """Global Page Layout with PicoCSS""" |
| 3 | +def PageLayout(title: str, content: list): |
| 4 | + """ |
| 5 | + The Base Layout. |
| 6 | + Injects PicoCSS (Styling), PrismJS (Syntax Highlight), and HTMX (Interactivity). |
| 7 | + """ |
5 | 8 | return Html( |
6 | 9 | Head( |
7 | 10 | Title(title), |
8 | | - # PicoCSS: Minimalist Semantic CSS |
| 11 | + # 1. PicoCSS: Semantic minimalist framework |
9 | 12 | Link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css"), |
10 | | - # PrismJS: For beautiful JSON Syntax Highlighting |
| 13 | + |
| 14 | + # 2. HTMX (CRITICAL FIX: Prevents page reloads) |
| 15 | + Script(src="https://unpkg.com/htmx.org@1.9.10"), |
| 16 | + |
| 17 | + # 3. PrismJS: For beautiful JSON highlighting |
11 | 18 | Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"), |
12 | 19 | Script(src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"), |
13 | 20 | Script(src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"), |
14 | | - Style("body { max-width: 800px; margin: 0 auto; padding: 20px; }") |
| 21 | + |
| 22 | + # Custom Styles |
| 23 | + Style(""" |
| 24 | + body { max-width: 900px; margin: 0 auto; padding: 20px; background-color: #11191f; } |
| 25 | + .json-box { border-radius: 8px; overflow: hidden; margin-top: 20px; } |
| 26 | + h1 { color: #00E5FF; } |
| 27 | + button.contrast { background-color: #00E5FF; border-color: #00E5FF; color: black; font-weight: bold; } |
| 28 | + .error-box { background: #3d1a1a; color: #ff8080; padding: 15px; border-radius: 8px; border: 1px solid #ff4d4d; } |
| 29 | + """) |
15 | 30 | ), |
16 | 31 | Body( |
17 | 32 | Header( |
18 | | - H1("🛡️ Structura", style="color: #00E5FF; margin-bottom: 0;"), |
| 33 | + H1("🛡️ Structura"), |
19 | 34 | P("The Unbreakable Data Architect", style="opacity: 0.7;"), |
20 | 35 | style="border-bottom: 1px solid #333; padding-bottom: 20px; margin-bottom: 40px;" |
21 | 36 | ), |
22 | | - Main(content), |
| 37 | + Main(*content), |
23 | 38 | Footer( |
24 | | - P("Powered by PydanticAI & Gemini 2.5", style="text-align: center; opacity: 0.5; font-size: 0.8rem;") |
| 39 | + P("Powered by PydanticAI & Gemini 2.5", style="text-align: center; opacity: 0.5; font-size: 0.8rem; margin-top: 50px;") |
25 | 40 | ) |
26 | 41 | ) |
27 | 42 | ) |
28 | 43 |
|
29 | | -def InputForm(): |
30 | | - """The HTMX-powered Input Form""" |
| 44 | +# ... (Rest of components.py remains the same) |
| 45 | +def HeroSection(): |
| 46 | + return Div( |
| 47 | + H2("Turn Chaos into Structure"), |
| 48 | + P("Paste messy emails, invoices, or resumes below. Get guaranteed, type-safe JSON back."), |
| 49 | + style="margin-bottom: 30px;" |
| 50 | + ) |
| 51 | + |
| 52 | +def ExtractionForm(): |
| 53 | + """ |
| 54 | + The Interactive Form. |
| 55 | + Uses HTMX (hx-post) to swap the result area without a page reload. |
| 56 | + """ |
31 | 57 | return Form( |
32 | | - Label("Raw Input Data", cls="label"), |
| 58 | + Label("Raw Input Data"), |
33 | 59 | Textarea( |
34 | 60 | name="text_input", |
35 | | - placeholder="Paste messy text here (Email, Invoice, Job Post)...", |
| 61 | + placeholder="e.g. Invoice #909 from CloudFix for $500...", |
36 | 62 | rows=8, |
37 | | - style="font-family: monospace;" |
| 63 | + style="font-family: monospace; background: #0b0f13; color: white; border: 1px solid #333;" |
38 | 64 | ), |
39 | 65 |
|
40 | | - Label("Target Schema", cls="label"), |
41 | | - Select( |
42 | | - Option("Invoice Extraction", value="invoice"), |
43 | | - Option("Resume Parsing", value="resume"), |
44 | | - Option("Email Summary", value="email"), |
45 | | - name="schema_type" |
| 66 | + Grid( |
| 67 | + Label("Target Schema", |
| 68 | + Select( |
| 69 | + Option("Invoice Extraction", value="invoice"), |
| 70 | + Option("Resume Parsing", value="resume"), |
| 71 | + Option("Generic Data", value="generic"), |
| 72 | + name="schema_type" |
| 73 | + ) |
| 74 | + ), |
| 75 | + # Empty div for grid balance or future features |
| 76 | + Div() |
46 | 77 | ), |
47 | 78 |
|
48 | 79 | Button("Extract Structure ⚡", cls="contrast", type="submit"), |
49 | 80 |
|
50 | | - # HTMX Magic: Swaps the result div without reloading page |
| 81 | + # HTMX Configuration |
51 | 82 | hx_post="/extract", |
52 | 83 | hx_target="#result-area", |
53 | | - hx_swap="innerHTML" |
| 84 | + hx_swap="innerHTML", |
| 85 | + hx_indicator="#loading" |
54 | 86 | ) |
55 | 87 |
|
56 | | -def ResultDisplay(json_data=None, error=None): |
57 | | - """Renders the output or error state""" |
58 | | - if error: |
59 | | - return Div( |
60 | | - H4("❌ Extraction Failed"), |
61 | | - P(error), |
62 | | - style="background: #3d1a1a; color: #ff8080; padding: 15px; border-radius: 8px; border: 1px solid #ff4d4d;" |
63 | | - ) |
64 | | - |
65 | | - if json_data: |
66 | | - return Div( |
67 | | - H4("✅ Validated JSON Output"), |
68 | | - # Pre/Code blocks triggered PrismJS highlighting |
69 | | - Pre(Code(json_data, cls="language-json")), |
70 | | - P("Schema validation passed.", style="color: #00E5FF; font-size: 0.8rem; margin-top: 10px;") |
71 | | - ) |
72 | | - |
73 | | - return Div(id="result-area") # Empty placeholder |
| 88 | +def LoadingIndicator(): |
| 89 | + return Div("⚙️ AI is processing...", id="loading", cls="htmx-indicator", style="color: #00E5FF; margin-top: 10px;") |
| 90 | + |
| 91 | +def SuccessDisplay(json_str: str): |
| 92 | + return Div( |
| 93 | + H4("✅ Validated Output", style="color: #4ade80; margin-bottom: 10px;"), |
| 94 | + Div( |
| 95 | + Pre(Code(json_str, cls="language-json")), |
| 96 | + cls="json-box" |
| 97 | + ), |
| 98 | + P("Schema Verified by PydanticAI", style="color: grey; font-size: 0.8rem; margin-top: 10px; text-align: right;") |
| 99 | + ) |
| 100 | + |
| 101 | +def ErrorDisplay(error_msg: str): |
| 102 | + return Div( |
| 103 | + H4("❌ Extraction Failed"), |
| 104 | + P(error_msg), |
| 105 | + cls="error-box" |
| 106 | + ) |
0 commit comments