Component System
Job Components
JobForm.tsx
Located: src/components/jobs/JobForm.tsx
Handles job creation and editing. Phase 0 changes:
- On mount: calls
GET /v1/stage-types→ caches response - Stage type dropdown populated from API (never hardcoded
STAGE_TYPESarray) - For each stage added, renders correct config panel based on
stageTypeConfig.questionBankConfig.questionType:'screening'→ renders<ScreeningQuestionsConfig />'dsa'→ renders<DsaProblemPicker />(Phase 3)'behavioral'→ renders<InterviewQuestionPicker />(Phase 1C)null→ no question bank panel
- Date/time fields hidden when
stageTypeConfig.schedulingConfig.type === 'async'
ScreeningQuestionsConfig.tsx
Located: src/components/jobs/ScreeningQuestionsConfig.tsx
Already fetches from API on mount. Reads system questions from GET /v1/screening-questions?source=system grouped by category.
Interview Components
ScheduleInterviewForm.tsx
Located: src/components/interviews/ScheduleInterviewForm.tsx
Phase 0 changes: Remove all hardcoded STAGE_FIELD_CONFIG. All conditional field rendering reads from StageTypeConfig:
StageTypeConfig field | What it controls |
|---|---|
schedulingConfig.requiresStartTime | Show date + time pickers |
schedulingConfig.requiresEndTime | Show end time picker |
schedulingConfig.hasExpiryDeadline | Show expiry deadline field |
automationConfig.hasSendLinkToggle | Show "send automated link" toggle |
Phase 1A note: "Customize for this candidate" modal — deferred, not implemented. The stageOverrides field exists on the Interview model for forward compatibility.
Interview Components (Phase 1B)
FeedbackForm.tsx
src/components/interviews/FeedbackForm.tsx — shared component used by both the post-call page and the late-submission dialog.
interface FeedbackFormProps {
interviewId: string;
role: 'recruiter' | 'candidate';
onSuccess: () => void;
onSkip?: () => void; // shows "Submit later" text link if provided
}Fields:
- Rating: 1–10 segmented button grid
- Recommendation: 4 pill buttons — recruiter sees "Strong Yes / Yes / No / Strong No"; candidate sees "Highly Recommend / Recommend / Not Recommend / Strong No"
- Traits: multi-select chip grid from the appropriate hardcoded list
- Comment: required for recruiter (min 5 chars,
z.string().min(5)), optional for candidate
Calls interviewApi.submitFeedback() (recruiter) or interviewApi.submitCandidateFeedback() (candidate).
Exports:
export const RECRUITER_TRAITS: string[]; // 10 recruiter-facing traits
export const CANDIDATE_TRAITS: string[]; // 10 candidate-facing traitsFeedback.tsx (reworked Phase 1B)
src/pages/Feedback.tsx — route: /feedback/:roomId
roomIdfromuseParams()= interview document_id(sincemeetingLink = /room/:interviewId)- Calls
interviewApi.getOne(roomId)to load interview title for context - Role detected from
dbUser.roles.includes('recruiter') - If
lastCallStatsexists (navigated from Room): shows compact call stats card - If no
lastCallStats(direct URL / page refresh): shows form without stats — no longer redirects to home - Renders
<FeedbackForm interviewId={roomId} role={role} onSuccess={...} onSkip={...} /> - Recruiter
onSuccess: shows success message → auto-navigates to/dashboard - Candidate
onSuccess: shows inline thank-you screen (no redirect) onSkip: navigates to/dashboard(recruiter) or/candidate/dashboard(candidate)
Room Components
Room.tsx
The live interview room. Existing WebRTC + Socket.io integration.
On call end: navigates to /feedback/:roomId (roomId = interviewId for pipeline-aware live interviews, or the nanoid lobby ID for legacy).
Phase 1C additions (deferred):
- If
isAuthenticated && interview.stageTypeKey === 'live_1on1': show "My Questions" panel - Call
GET /v1/interviews/:id/private-questions - Each question has a "Push to candidate" button → socket event
push_question - Candidate's screen shows pushed question in a visible card (but never sees
interviewerHintsorexpectedAnswerGuide)
Candidate Components (Phase 2+)
Dashboard.tsx
src/pages/candidate/Dashboard.tsx
- Lists all pipelines sorted by
lastActivityAt - Shows
jobSnapshot,candidateFacingStatus, stage status dots - No internal data
PipelineDetail.tsx
src/pages/candidate/PipelineDetail.tsx
- Stage timeline with
candidateStatusper stage - Accept/Decline RSVP buttons for
status === 'scheduled'live stages candidateAggregateScorebadge on completed stages
PastSession.tsx
src/pages/candidate/PastSession.tsx
- Renders own screening answers, DSA code, AI conversation transcript
- Zero AI scores, feedback, or internal results
Public Session Pages (No Auth)
Screening.tsx
src/pages/candidate/Screening.tsx — route: /candidate/screening?token=TOKEN
- Extract
tokenfrom query string GET /v1/screening/:token→ load questions and session metadata- Multi-step text Q&A — one question per screen, progress indicator
POST /v1/screening/:token/starton first answerPOST /v1/screening/:token/submiton final submission- Thank-you / completion screen
DeclineConfirmation.tsx
src/pages/candidate/DeclineConfirmation.tsx — route: /candidate/decline/:token
Loaded from the email decline link. Before the candidate confirms:
- Optional free-text reason (max 1 000 chars)
- Multi-select reason chips (pre-set options)
- "Confirm Decline" CTA →
POST /v1/interviews/decline/:declineTokenwith{ reason?, tags? } - Success/thank-you screen after confirmation
DsaSession.tsx (Phase 3)
src/pages/DsaSession.tsx — route: /dsa/:token
- Monaco editor with language switcher
- Problem statement panel
- "Run" (visible test cases) + "Submit" (all test cases)
- Timer countdown
AiInterviewSession.tsx (Phase 4)
src/pages/AiInterviewSession.tsx — route: /ai-session/:token
- Text or voice interface — determined by
StageTypeConfig.toolsConfig.voiceInterface - Optional whiteboard/IDE enabled from
StageTypeConfig.toolsConfig(not hardcoded)
Recruiter Dashboard Pages
CandidatePipelineTab.tsx (Phase 1A)
src/components/jobs/CandidatePipelineTab.tsx — embedded as the "Candidates" tab inside JobDetails.tsx, not a standalone page.
GET /v1/pipeline?jobId=:jobIdon mount; all filtering is client-side- Client-side search (debounced, name/email) + stage filter dropdown
- Pagination:
PAGE_SIZE = 10, Prev/Next controls - Each row: avatar initial, name + email, current stage,
{completed}/{total}count, status badge onSelectCandidate(pipelineId)callback → opensCandidateDetailPanelin aResponsiveSheet
CandidateDetailPanel.tsx (Phase 1A + 1B)
src/components/jobs/CandidateDetailPanel.tsx — embedded as a ResponsiveSheet modal inside JobDetails.tsx, not a standalone page. Prop-driven: pipelineId: string.
GET /v1/pipeline/:id→ stage timeline with status badges and timestamps- AI report viewer: expanding a completed
automated_screeningstage showsscreeningAiReport(score, summary, recommendation, strengths, concerns) + Q&A pairs — recruiter only - Decline data: candidate's optional reason text and selected chips displayed in the stage card
- Live stage feedback (Phase 1B):
- Expanding a
live_1on1orculture_fit_hrstage card fetches interview data viainterviewApi.getOne(interviewId) - If no feedback yet: shows amber "Feedback pending" badge + "Submit Feedback" button
- If feedback exists: shows summary card — rating badge, recommendation chip (color-coded), trait pills, comment excerpt, timestamp
- Stage header shows an amber "Feedback" button for stages with an interview but no feedback (not yet completed)
- Button opens a
Dialogcontaining<FeedbackForm role="recruiter">— on success: closes dialog + re-fetches pipeline
- Expanding a
- Unlock stage: "Unlock Next Stage" button; shows warning dialog if preceding stages are incomplete. If a preceding live stage lacks feedback, the API returns
400 requiresFeedback: trueeven withforce: true - Schedule CTA: for an
unlockedstage with no interview, a "Schedule" button closes the candidate modal and opens the schedule modal withparticipantName,participantEmail,roundpre-filled - Notes: recruiter notes panel with
POST /v1/pipeline/:id/notes - Global status selector:
PATCH /v1/pipeline/:id/status
src/pages/recruiter/CandidateDetail.tsxis a thin route wrapper reusingCandidateDetailPanelfor the legacy full-page route (/dashboard/jobs/:jobId/candidates/:pipelineId). This route exists inApp.tsxbut is not linked from the main UI.
CandidatePool.tsx (Phase 1A)
src/pages/recruiter/CandidatePool.tsx — route: /dashboard/pool
Top-level page that owns selectedParticipantId state and wires the split-pane layout. No URL param changes on selection — matches Pipeline.tsx pattern. Mobile responsive: left pane hides when profile is open; back button in profile restores list.
CandidatePoolList.tsx (Phase 1A)
src/components/pool/CandidatePoolList.tsx — left pane.
- Three compact stat cards at top: Total / Active / Hired from
summaryin the pool response - Search input (400 ms debounce, min 2 chars, name + email)
- Status filter dropdown + Sort dropdown
- Paginated candidate rows: avatar initials, name, email, status badge, job count, last activity date
- Calls
pipelineApi.getPool()on mount and on any param change
CandidatePoolProfile.tsx (Phase 1A)
src/components/pool/CandidatePoolProfile.tsx — right pane.
- Participant header: large avatar, name, email, stats badges (jobs, interviews, stages completed)
- Tags: union of all
pipeline.tagsacross jobs - Jobs tab: one card per pipeline with title, status badge, mini stage progress bar (
completed/total), last activity. Click →ResponsiveSheet→CandidateDetailPanel(full stage timeline, AI report, notes, status selector) - Activity tab: chronological timeline built client-side from
stageProgressiontimestamps across all pipelines. No extra API call. - Calls
pipelineApi.getPoolProfile()onparticipantIdchange
InterviewQuestionBank.tsx (Phase 1C)
Route: /interview-questions
- List + create private questions
- Filter by type, tags
- Preview
interviewerHints
Analytics.tsx (Phase 6)
Route: /analytics
- Pipeline funnel chart
- Time-to-hire breakdown
- Gated by
featureSnapshot.advancedAnalytics(upgrade prompt if false)