Reviewer-friendly AI CSV importer for messy CRM lead spreadsheets. It previews CSV files locally, waits for explicit confirmation, processes rows through an Express async job, validates every AI-shaped result with shared Zod schemas, and exports Listwright-ready CRM data.
- Frontend / Web App: https://listwright-web.vercel.app/
- Backend / Render API: https://listwright-api.onrender.com
- Open the web app.
- Load a sample CSV or choose a local CSV.
- Review the local preview. No backend or AI import happens before user confirmation.
- Click Confirm import.
- Watch batch progress, imported count, skipped count, and failed batches.
- Inspect parsed records, skipped records, mapping notes, warnings, confidence, and before/after row comparisons.
- Retry failed batches if needed.
- Export CRM CSV or full review JSON.
flowchart LR
Browser["Next.js frontend\nlocal PapaParse preview"] -->|Confirm import only| API["Express API"]
API --> Parser["CSV parser\nrow limit"]
Parser --> Prep["Deterministic preprocessing\nemails, phones, dates, duplicates"]
Prep --> AI["OpenAI structured output\nor deterministic fallback without key"]
AI --> Validator["Backend normalization\nshared Zod validation"]
Validator --> Memory["In-memory jobs\nrecords, skipped, mapping notes"]
Memory --> Exports["CRM CSV\nreview JSON"]
npm install
cp .env.example .env
npm run devFrontend: http://localhost:3000
Backend: http://localhost:4000
The app works without OPENAI_API_KEY by using the deterministic extractor. Add OPENAI_API_KEY to exercise OpenAI Structured Outputs.
Backend:
OPENAI_API_KEY: optional locally, required for true AI extraction.OPENAI_MODEL: defaults togpt-4o-mini.PORT: defaults to4000.CORS_ORIGIN: defaults to local frontend in.env.example.IMPORT_ROW_LIMIT: defaults to1000.
Frontend:
NEXT_PUBLIC_API_BASE_URL: defaults tohttp://localhost:4000.
docker compose up --buildThe web container runs on 3000, the API on 4000.
npm run typecheck
npm run lint
npm run test
npm run buildOptional browser flow:
PLAYWRIGHT_CLI=playwright-cli npm run test:e2etest:e2e builds the app, starts the API and web server, loads the mixed-leads sample in a real browser, confirms import, waits for terminal progress, and verifies CSV/JSON export links. Set PLAYWRIGHT_CLI to any executable compatible with the playwright-cli command interface.
- AI prompt engineering: each batch prompt includes the target CRM schema, allowed enum values, skip rules, deterministic signals, ambiguity handling, and a no-invention rule. Responses are checked again with a runtime Zod schema before normalization.
- Field extraction and mapping: deterministic preprocessing detects emails, phones, dates, duplicate rows, likely status/source values, and extra contacts. Header aliases and value evidence guide mapping, while ambiguous mappings are surfaced as notes instead of silently guessed.
- Backend quality: Express routes use stable contracts, bounded multipart uploads, row limits, safe filenames, security headers, paginated result endpoints, async batch processing, terminal status tracking, and retryable failed batches.
- Frontend quality: the Next.js UI has local-only preview, drag-and-drop upload, a four-step workflow, sticky scrollable tables, progress metrics, parsed/skipped/mapping/raw JSON views, exports, accessible empty states, and Sonner feedback.
- Code quality: TypeScript is checked across workspaces, shared Zod schemas define API boundaries, importer-specific UI pieces live under
apps/web/src/components/importer/, and backend behavior is covered by unit tests. - Production notes: Dockerfiles and Compose are included. Jobs are intentionally in memory for the demo, so a deployed instance should remain single-node until Postgres and a durable queue replace the current store.
Bonus coverage: drag-and-drop, progress indicators, retry failed batches, Docker setup, unit tests, and an end-to-end sample flow are implemented. Streaming parsing, table virtualization, dark mode, and durable deployment infrastructure are intentionally out of scope for the reviewer-fast demo.
Deterministic fallback:
- Leave
OPENAI_API_KEYblank in.env. - Run
npm run dev. - Open
http://localhost:3000. - Load Mixed leads, confirm import, inspect parsed/skipped records, then export CSV and JSON.
OpenAI structured extraction:
- Add
OPENAI_API_KEYto.env. - Optionally set
OPENAI_MODEL. - Run
npm run dev. - Repeat the sample flow and verify the same backend validation, warnings, mapping notes, and exports remain visible.
Recommended:
- Frontend: Vercel, set
NEXT_PUBLIC_API_BASE_URLto the backend URL. - Backend: Render or Railway, set
OPENAI_API_KEY,OPENAI_MODEL,IMPORT_ROW_LIMIT, andCORS_ORIGIN.
Use one backend instance on Render/Railway because jobs are in memory. In-memory jobs reset on server restart; production would use Postgres for job/results storage and Redis or a queue for background processing state.
GET /healthPOST /api/importsGET /api/imports/:jobIdGET /api/imports/:jobId/records?page=1&limit=100GET /api/imports/:jobId/skipped?page=1&limit=100POST /api/imports/:jobId/retryGET /api/imports/:jobId/export.csvGET /api/imports/:jobId/export.json
- No AI/backend import happens before user confirmation.
- LLM output is treated as untrusted and validated with shared Zod schemas.
- The backend owns final normalization, skip rules, confidence/warnings, traceability, and exports.
- Records are skipped only when both email and mobile are missing.
crm_statusanddata_sourcemust be allowed values or blank.created_atmust be parseable bynew Date(created_at)or blank.- The first email/mobile becomes primary; extras go into
crm_note. - CRM CSV export contains exactly:
created_at,name,email,country_code,mobile_without_country_code,company,city,state,country,lead_owner,crm_status,crm_note,data_source,possession_time,descriptionConfidence, warnings, original rows, and mapping notes appear only in the UI and JSON export.
Sample files live in samples/ and are also exposed to the frontend under apps/web/public/samples/:
mixed-leads.csvmessy-contacts.csvlistwright_test_leads.csv— exact 40-row test file used for the screenshots and demo flow.
- In-memory jobs keep the reviewer demo simple, but they reset on restart and require a single backend instance.
- The deterministic fallback makes local review possible without credentials. With
OPENAI_API_KEY, the API attempts structured LLM mapping in five-row batches so timeouts remain isolated and retry resends only small failed batches. Every output still passes backend validation. - Frontend virtualization is intentionally lightweight: preview rendering is capped for review speed while backend processing honors
IMPORT_ROW_LIMIT. - No auth, Postgres, Prisma, admin views, import history, billing, workspaces, or CRM write-back are included.


