A React web app for analyzing resumes and job descriptions from a hiring committee perspective.
- Frontend: React 19 + Vite + Tailwind CSS
- Backend: Vercel Serverless Functions (Node.js)
- AI: Anthropic Claude API (claude-sonnet-4-6)
kate/
├── api/
│ ├── config.js # Shared API configuration
│ ├── analyze-v1.js # Initial resume + JD analysis + Q1
│ ├── analyze-q2.js # Generate question 2 based on Q1 answer
│ └── analyze-final.js # Final comprehensive analysis
├── src/
│ ├── components/
│ │ ├── LandingInput.jsx # Stage 1: Resume + JD input
│ │ ├── AnalysisV1.jsx # Stage 2a: Initial analysis display
│ │ ├── Question.jsx # Stage 2b/3: MC question + optional context
│ │ ├── FinalAnalysis.jsx # Stage 4: Final analysis display
│ │ ├── Conversion.jsx # Stage 5: Setup + handoff summary
│ │ └── Loading.jsx # Loading spinner component
│ ├── utils/
│ │ └── pdfParser.js # Client-side PDF parsing
│ ├── App.jsx # Main app with stage management
│ └── index.css # Tailwind + Spectral font
├── vercel.json # Vercel routing configuration
└── tailwind.config.js # Tailwind customization
Required environment variables in Vercel:
| Name | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key for Claude access |
To add/update environment variables:
vercel env add ANTHROPIC_API_KEY production# Install dependencies
npm install
# Start dev server (frontend only)
npm run devThe app will be available at http://localhost:5173
Note: API functions require Vercel environment variables. Use vercel env pull to get local copies.
# Deploy to preview
vercel
# Deploy to production
vercel --prod --forceThe vercel.json configures:
- API functions:
api/**/*.js→@vercel/node - Frontend build:
package.json→@vercel/static-build(distDir:dist) - Routes:
/api/analyze-v1,/api/analyze-q2,/api/analyze-final
Initial analysis and first positioning question.
Request Body:
{
"resume": "string (max 8000 chars)",
"jobDescription": "string (max 4000 chars)"
}Response Schema:
{
"valid_input": boolean,
"role_fit": "Uphill Battle | Positioning Play | Strong Fit",
"level_inference": "string",
"fit_tier": "string",
"hiring_committee_read": "string",
"gaps": [{"label": "string", "detail": "string", "severity": "string"}],
"what_strong_looks_like": "string",
"complement_skill": "string",
"priority_action": "string",
"question_1": {
"question_text": "string",
"context": "string",
"dimension": "string",
"options": [{"label": "A", "text": "string"}, ...]
}
}Second question generation based on first answer.
Request Body:
{
"resume": "string",
"jobDescription": "string",
"v1Analysis": {},
"q1Answer": "string | null",
"q1QuestionText": "string",
"additionalContext": "string | null (max 1000 chars)"
}Final analysis with all accumulated context.
Request Body:
{
"resume": "string",
"jobDescription": "string",
"v1Analysis": {},
"q2Data": {},
"q1Answer": "string | null",
"q2Answer": "string | null",
"q1Context": "string | null",
"q2Context": "string | null",
"q1QuestionText": "string",
"q2QuestionText": "string"
}All user-supplied inputs are wrapped in XML-style delimiters before injection into prompts:
<resume>{{resume_text}}</resume>
<job_description>{{jd_text}}</job_description>
<q1_answer>{{q1_answer_text}}</q1_answer>
<q2_answer>{{q2_answer_text}}</q2_answer>
<additional_context>{{context_text}}</additional_context>
Empty tags are omitted. Only relevant tags for each call are included.
| Field | Limit |
|---|---|
| Resume | 12,000 characters |
| Job Description | 6,000 characters |
| Additional Context (per question) | 1,000 characters |
Limits are enforced both client-side and server-side (returns HTTP 400).
A hidden honeypot field catches bots. If filled, the request is silently rejected (HTTP 200 with empty response, no API call fired).
The valid_input boolean in the V1 response allows Kate to flag non-genuine inputs. Frontend displays an error state if valid_input === false.
Rate limiting placeholders are included in each serverless function. Implement by adding logic in the marked placeholder block.
0: LANDING → Resume + JD input
1: LOADING_V1 → Loading state
2: ANALYSIS_V1 → Initial analysis + Q1
3: LOADING_Q2 → Loading state
4: QUESTION_2 → Q2 + optional context
5: LOADING_FINAL → Loading state
6: FINAL_ANALYSIS → Final analysis display
7: CTA → "Meet Kate" prompt
8: CONVERSION → Setup steps + handoff summary
9: VALIDATION_ERROR → Error state with Try Again (preserves inputs)
- Background: Off-white (
#FAF8F5) - Text: Black (
#1A1A1A) - Accent: Maroon (
#8B2635) - Gold:
#C9A84C(role fit spectrum active state, counter warning) - Font: Spectral (serif)
- The PDF parser uses pdf.js to extract text client-side — no PDF data is sent to the server
- All API calls use temperature 0.3 for analysis endpoints and 0.5 for question generation
- JSON validation with one retry is implemented on all API responses
- Model name is stored in
/api/config.js— updateMODEL_NAMEto use a different Claude model