Components
Key UI components that make up the ETL pipeline interface. All components are client-side ("use client") and built with Mantine v7 and Phosphor Icons.
Styling Conventions
All 8 components (StructureReview, ResultsPanel, ComparisonView, AccuracyFunnel, ProgressStepper, LandingHero, UploadZone, PasswordGate) use semantic theme tokens defined in theme.other instead of hardcoded hex values. The following tokens are available:
| Token | Value | Purpose |
|---|---|---|
theme.other.inputBg | #eceadd | Cream background for inputs and cards |
theme.other.blueAccent | #3d8ab5 | Blue accent for info/unit count |
theme.other.goldAccent | #c9a227 | Gold accent for warnings/flagged items |
theme.other.brownAccent | #6b5c3e | Brown accent for table-related labels |
theme.other.subtleBorder | rgba(61,61,61,0.15) | Light borders |
theme.other.subtleBg | rgba(61,61,61,0.04) | Very faint background tint |
theme.other.faintBorder | rgba(61,61,61,0.1) | Table/notification borders |
Guideline: Use semantic theme tokens from theme.other.* instead of hardcoded hex values. One-off colors that appear in only one component may remain inline.
ProgressStepper
File: components/ProgressStepper.tsx
Displays the 4-step pipeline progress with a milestone timeline, ETA estimation, and browser notification prompt.
Props
interface ProgressStepperProps {
pipelineState: string; // From useEtlPipeline state
progress: JobProgress; // From useJobProgress
onCancel?: () => void; // Cancel handler
}Steps
The stepper has four labeled steps plus a completion state:
| Step | Label | Description |
|---|---|---|
| 0 | Upload | Sending file |
| 1 | Read | Finding Tables |
| 2 | Review | Confirm structure |
| 3 | Extract | Pulling Units |
| — | Completed | (empty) |
Step descriptions use the color-coded entity system: Table is styled with theme.other.brownAccent and Unit with theme.other.blueAccent.
ETA Calculation
The component includes a useHistoricalEta hook that queries up to 20 completed jobs from etl_jobs and runs a simple linear regression to compute a model with baseMs (fixed overhead) and msPerRow (marginal cost per row). The estimated total time is baseMs + msPerRow * total_rows, minus elapsed time.
Fallback estimates when no historical data is available:
- During analysis: “~1-2 min”
- During extraction: “~1-3 min”
Elapsed Timer
A useTimer hook tracks wall-clock time while the pipeline is active. Displays formatted elapsed time (e.g., 45s, 1m 30s).
Status Bar
Below the progress bar, a three-column layout shows:
- Left: Elapsed time
- Center: Cancel link (while active), “All done!” (on complete), or error message (on failure)
- Right: ETA remaining
Milestone Timeline
The MilestoneTimeline sub-component renders a Mantine Timeline showing every stage_message the pipeline has emitted. Features:
- Auto-scrolls to the latest milestone
- Max height of 240px with overflow scrolling
- Completed milestones show a checkmark bullet; the current milestone shows a spinning icon
- Messages are colorized using the entity system (see below)
Browser Notifications
The NotificationPrompt sub-component appears after 5 seconds of active processing (including during the awaiting_review state). It prompts the user to enable browser notifications. When the pipeline completes or enters awaiting_review with notifications enabled, it fires a system notification — either prompting the user to review the structure or showing the Unit count and filename on completion.
Entity Colorization
The colorizeMessage function parses milestone messages and applies color-coded styling to domain entities:
| Entity | Color | Example |
|---|---|---|
| File | theme.other.textPrimary (dark) | “Uploading your File…” |
| Sheet / Sheets | theme.other.greenAccent (green) | “Found 3 Sheets with 1,231 rows” |
| Table / Tables | theme.other.brownAccent (brown) | “Found 5 Tables across 3 Sheets” |
| Unit / Units | theme.other.blueAccent (blue) | “Got 18 Units so far…” |
ResultsPanel
File: components/ResultsPanel.tsx
Displays the final results after pipeline completion. Shows property info, Unit count, quality badges, accuracy funnel, and a toggle for the comparison view.
Props
interface ResultsPanelProps {
progress: JobProgress;
jobId: string;
onReset: () => void;
isDemo?: boolean;
}Layout
The panel is a card with cream background (theme.other.inputBg) containing:
- Header — “Results” title with Download and “Process Another” buttons
- Property info — Property name, report date (formatted to long date), source filename
- Units Extracted — Badge with Unit count (with expected data rows denominator if available, e.g., “106/112”) plus extraction method badge (
AI Extracteddisplayed for all extractions) - Spot Check — Confidence score badge (green >= 80, yellow >= 50, red < 50)
- Accuracy — Inline
AccuracyFunnelcomponent (280px wide) - Quality — Error count badge (red if > 0, green if 0) and warning count badge (yellow if > 0, green if 0)
- Details accordion — Expandable sections for spot-check discrepancies, errors, and warnings (warnings capped at 20 visible with overflow count)
- Comparison toggle — Button to show/hide the
ComparisonView
Download Flow
In non-demo mode, clicking Download:
- Fetches a signed URL from
GET /api/download?job_id={jobId} - Downloads the file as a blob (to avoid cross-origin issues with the
downloadattribute) - Creates a temporary
<a>element to trigger the browser download - Falls back to filename
rent_roll_standardized.xlsx
In demo mode, the download button is disabled.
AccuracyFunnel
File: components/AccuracyFunnel.tsx
A stacked horizontal progress bar showing the breakdown of Unit extraction quality.
Props
interface AccuracyFunnelProps {
breakdown: UnitBreakdown; // { successful, flagged, failed, total }
}Segments
| Segment | Color | Label |
|---|---|---|
| Successful (parsed) | theme.other.greenAccent (green) | Count of Units parsed cleanly |
| Flagged | theme.other.goldAccent (gold) | Count of Units with warnings |
| Failed | #c0392b (red) | Count of Units that failed extraction |
Each segment is proportional to total. Labels inside segments are only shown when the segment exceeds 12% width. Below the bar, a legend shows colored dots with counts and the word “Units” styled in theme.other.blueAccent.
Returns null when total is 0.
ComparisonView
File: components/ComparisonView.tsx
Two-column side-by-side view comparing source spreadsheet cells against cleaned canonical output for each Unit.
Props
interface ComparisonViewProps {
jobId: string;
isDemo?: boolean;
}Data Loading
- Demo mode: Uses
DEMO_COMPARISON_DATAfromuseDemoMode(6 hardcoded Montrose Units) - Live mode: Fetches from
GET /api/comparison?job_id={jobId}with auth headers
Navigation
- Prev/Next buttons to step through Units
- Unit selector dropdown showing all Unit IDs
- Counter displaying “Unit X of Y” with “Unit” styled in
theme.other.blueAccent
Paired-Row Layout
The comparison is rendered as an aligned table with six columns: Source header, Source value, arrow, Canonical field, Canonical value, and tag badge. Each row pairs a source cell with its corresponding canonical field.
- Row number shown at the top
- Summary badges above the table show counts of “cleaned” and “inferred” differences
- Each row is tagged as:
- direct — source value matches canonical value exactly (no badge shown)
- cleaned — value was reformatted or normalized (yellow “cleaned” badge, gold-tinted row background)
- inferred — canonical field has no source column (blue “inferred” badge, blue-tinted row background)
- Canonical field names have tooltips with descriptions from the
FIELD_DESCRIPTIONSdictionary
ComparisonUnit Type
interface ComparisonUnit {
unitId: string;
sourceRowNumber: number;
sourceCells: { column: string; header: string; value: string }[];
canonicalFields: Record<string, unknown>;
differences: ComparisonDiff[];
}
interface ComparisonDiff {
canonicalField: string;
sourceColumn: string;
sourceValue: string;
canonicalValue: unknown;
type: "mapped" | "transformed" | "missing";
}StructureReview
File: components/StructureReview.tsx
Displays the AI-detected column mapping and allows the user to override field assignments before extraction proceeds.
Props
interface StructureReviewProps {
structure: {
column_mapping?: Record<string, string>;
column_headers?: Record<string, string>;
header_row?: number;
data_start_row?: number;
data_end_row?: number;
charge_orientation?: string;
multi_row_per_unit?: boolean;
property_name?: string;
report_date?: string;
notes?: string;
};
onConfirm: (overrides?: Record<string, unknown>) => void;
onReanalyze?: () => void;
isConfirming?: boolean;
}Canonical Fields
The component defines 36 canonical field options that columns can be mapped to:
unit_id, floor_plan, sqft, beds, baths, unit_status, tenant_name, move_in, move_out, lease_start, lease_end, lease_term_months, is_mtm, market_rent, charge_base_rent, charge_pet, charge_parking, charge_storage, charge_utilities, charge_trash, charge_cable_internet, charge_pest_control, charge_amenity_fee, charge_washer_dryer, charge_package_locker, charge_insurance, charge_deposit_waiver, charge_concession, charge_mtm_fee, charge_employee_discount, charge_subsidy, charge_cam, charge_admin_fees, charge_other, deposit_required, deposit_on_hand, balance, plus (unmapped) (displayed as “Skip”).
Layout
- Header — “Review Structure” title with charge orientation badge (e.g., “horizontal layout”)
- Description — Instructions to confirm or override the detected mapping
- Metadata accordion — Expandable section showing property name, report date, header row, data row range, multi-row flag, and notes
- Column mapping table — Three columns: Original Column (badge with column header text), Mapped To, Override (dropdown). Overridden fields show the original with a strikethrough.
- Action buttons — “Re-analyze” (optional) and “Confirm & Extract” (or “Apply Overrides & Extract” if overrides exist)
Override Behavior
When the user selects overrides, they are tracked in local state. On confirm:
- If overrides exist, calls
onConfirm({ column_mapping: mergedMapping })with the original mapping plus overrides applied - If selecting
(unmapped), removes that column from the mapping - If no overrides, calls
onConfirm()with no arguments
UploadZone
File: components/UploadZone.tsx
Drag-and-drop file upload component using Mantine’s Dropzone.
Props
interface UploadZoneProps {
onDrop: (file: File) => void;
loading?: boolean;
}Configuration
| Setting | Value |
|---|---|
| Accepted types | .xlsx, .xls (MS Excel MIME types), .csv |
| Max file size | 10 MB |
| Max files | 1 |
| Min height | 260px |
The drop zone has a cream background (theme.other.inputBg) with a green dashed border on drag-accept state. Displays a FileXls icon, a primary prompt, and an accepted formats hint.
LandingHero
File: components/LandingHero.tsx
Public landing page hero section with two CTAs.
Props
interface LandingHeroProps {
onPreviewDemo: () => void;
}Layout
Centered stack containing:
- Title: “Upload Your Rent Roll”
- Subtitle: Description of the product (max 480px width)
- Buttons:
- “Preview Demo” (filled dark, Play icon) — triggers
onPreviewDemowhich starts theuseDemoModesimulation - “Try Live Version” (outlined dark, ArrowRight icon) — links to
/pipeline(the password-gated live pipeline)
- “Preview Demo” (filled dark, Play icon) — triggers
PasswordGate
File: components/PasswordGate.tsx
Session-based authentication wrapper for the live pipeline. Protects the /pipeline route behind a simple password check.
Usage
<PasswordGate>
<PipelineApp />
</PasswordGate>Behavior
- On mount, checks
sessionStoragefor the keyrollformat_demo_auth - If found, renders children immediately
- If not found, renders a centered password form
- On correct password, stores the value in
sessionStorageand renders children - On wrong password, shows an inline error message
The session persists for the browser tab lifetime (cleared when the tab closes). Includes a “Back to preview” link that navigates to /.