Iris AI Copilot — Conversational Candidate Sourcing
Recruiters describe who they want in plain English. Getting that from chat into structured filters and matches without chaos was the interesting part.
Overview
I built and iterated on an AI-powered recruiting copilot for an HR tech platform. Recruiters describe who they are looking for in natural language; the system uses GPT to extract structured candidate requirements and drives a multi-field filter modal, match previews, and job creation from the same conversation. The experience is real-time (streaming responses), with conversation memory that resets on refresh, and full integration into the corporate dashboard with analytics tracking.
Tech Stack
Impact & Scale
- Recruiters can source candidates via conversation instead of form-heavy flows
- Structured extraction of job titles, industries, locations, and experience from free-form chat
- Deterministic AI responses (temperature 0) for consistent match criteria
Key Challenges & Solutions
Keeping AI output consistent for downstream features
Match criteria feed into search and filters. Non-deterministic AI would have made the UX unpredictable. We set temperature to 0 on the completion API and added validation to strip unexpected inferred fields so the rest of the pipeline always received a stable shape.
Orchestrating conversation state and API flow
The copilot needed to manage conversation history, streaming, industry semantic search with dynamic limits, and inference extraction in one flow. A single custom hook encapsulated conversation state, API calls to the completion endpoint, and extraction logic so the UI stayed simple and testable.
Filter modal complexity and null safety
The filter modal combined sliders, location autocomplete, industry semantic search, inferred vs user-provided value display, and conditional logic. We used defensive coding and optional chaining throughout so that slow or partial API responses never produced 500s or white screens.
Technical Highlights
- Real-time conversational UI with dynamic input height, message streaming, and conversation memory scoped to the session
- Backend API route for GPT completions with prompt engineering for structured candidate requirement extraction
- Filter modal with years-of-experience slider, location autocomplete, industry search, and visual distinction between AI-inferred and user-specified values
- Match-query display and edit components for AI-extracted criteria; job creation flow triggered directly from conversation results
- Conversation lifecycle handling: new chat titles, error recovery, and disabling conversation switch while the model is streaming
Role and scope
I owned the frontend experience and the backend API route for the AI recruiter feature. The goal was to reduce friction for recruiters: instead of filling out many filter fields, they could describe their ideal candidate in chat and get structured criteria, suggested filters, and the option to create a job from the same flow.
Architecture
The flow is driven by a custom hook that holds conversation state and calls a Next.js API route. The route sends the conversation to OpenAI, runs post-processing (validation, field stripping), and returns structured data. The frontend uses that data to populate the filter modal and match-query views. Industry and location data come from existing platform services (semantic search, places autocomplete), so the copilot stays aligned with the rest of the product’s data model.
Lessons learned
- Temperature and shape: For any AI output that feeds into code (filters, search, job creation), deterministic output is worth it. Temperature 0 plus strict validation avoids “it worked in the UI but broke the API” issues.
- One hook, one responsibility: Keeping conversation, API, and extraction in one hook made the UI components thin and made it easier to fix bugs (e.g. conversation memory, streaming state) in one place.
- Perceived performance: Showing a loading skeleton as soon as the user switches tabs or starts an action matters more than optimizing the API when the main complaint is “something flashed wrong.” UI-level loading signals need to fire on interaction, not when the request starts.
Interested in discussing this project or working together? Get in touch.