types.ts
1 // ============================================================================ 2 // GapZero — Type Definitions 3 // All interfaces match the PRD data schemas and SAMPLE_ANALYSIS.json structure 4 // ============================================================================ 5 6 import type { GitHubAnalysis } from '@/lib/prompts/github-analysis'; 7 import type { CoverLetter } from '@/lib/prompts/cover-letter'; 8 9 // Re-export for consumers 10 export type { GitHubAnalysis, CoverLetter }; 11 12 // --- Input Types --- 13 14 export interface CareerQuestionnaire { 15 currentRole: string; 16 targetRole: string; 17 targetRole2?: string; // Optional alternative target role 18 targetRole3?: string; // Optional alternative target role 19 yearsExperience: number; 20 country: string; 21 workPreference: 'remote' | 'hybrid' | 'onsite' | 'flexible'; 22 currentSalary?: number; 23 targetSalary?: number; 24 jobPosting?: string; // Legacy single posting (backward compat) 25 jobPostingUrl?: string; // URL of a job posting (fetched server-side) 26 jobPostings?: JobPostingInput[]; // Multiple postings for richer context 27 language?: string; // 'en' | 'ro' | 'de' — for localized AI responses 28 linkedInProfile?: string; // Raw LinkedIn profile text for supplementary data 29 githubUrl?: string; // GitHub profile URL for repo/language analysis 30 additionalContext?: string; // User-provided context about career gaps, freelance work, etc. 31 } 32 33 export interface JobPostingInput { 34 text: string; // Full job posting text 35 url?: string; // Source URL if fetched 36 title?: string; // Extracted job title 37 } 38 39 // --- Output Types --- 40 41 export interface AnalysisResult { 42 metadata: AnalysisMetadata; 43 fitScore: FitScore; 44 strengths: Strength[]; 45 gaps: Gap[]; 46 roleRecommendations: RoleRecommendation[]; 47 actionPlan: ActionPlan; 48 salaryAnalysis: SalaryAnalysis; 49 jobMatch?: JobMatch; 50 atsScore?: ATSScoreResult; 51 profile?: ExtractedProfile; 52 githubAnalysis?: GitHubAnalysis; 53 coverLetter?: CoverLetter; 54 interviewPrep?: InterviewPrep; 55 } 56 57 export interface AnalysisMetadata { 58 analyzedAt: string; 59 cvFileName: string; 60 targetRole: string; 61 country: string; 62 /** Original job posting text (for CV re-scoring) */ 63 jobPosting?: string; 64 /** GitHub profile URL for repo analysis */ 65 githubUrl?: string; 66 /** Warning message if PDF text extraction quality was suboptimal */ 67 pdfQualityWarning?: string; 68 /** Tracks which data came from Claude vs fallback */ 69 dataSources?: Record<string, 'claude' | 'fallback'>; 70 /** Whether a real CV was uploaded (false = LinkedIn PDF only) */ 71 hasRealCV?: boolean; 72 /** Pipeline coherence warnings (e.g., fitScore vs matchScore divergence) */ 73 warnings?: string[]; 74 } 75 76 export interface FitScore { 77 score: number; 78 label: 'Strong Fit' | 'Moderate Fit' | 'Stretch' | 'Significant Gap'; 79 summary: string; 80 } 81 82 export interface Strength { 83 title: string; 84 description: string; 85 relevance: string; 86 tier: 'differentiator' | 'strong' | 'supporting'; 87 } 88 89 export interface Gap { 90 skill: string; 91 severity: 'critical' | 'moderate' | 'minor'; 92 currentLevel: string; 93 requiredLevel: string; 94 impact: string; 95 closingPlan: string; 96 timeToClose: string; 97 resources: string[]; 98 } 99 100 export interface RoleRecommendation { 101 title: string; 102 fitScore: number; 103 salaryRange: SalaryRange; 104 reasoning: string; 105 exampleCompanies: string[]; 106 timeToReady: string; 107 } 108 109 export interface SalaryRange { 110 low: number; 111 mid: number; 112 high: number; 113 currency: string; 114 } 115 116 export interface ActionPlan { 117 thirtyDays: ActionItem[]; 118 ninetyDays: ActionItem[]; 119 twelveMonths: ActionItem[]; 120 } 121 122 export interface ActionItem { 123 action: string; 124 priority: 'critical' | 'high' | 'medium'; 125 timeEstimate: string; 126 resource: string; 127 resourceUrl?: string; 128 expectedImpact: string; 129 } 130 131 export interface SalaryAnalysis { 132 currentRoleMarket: MarketSalary; 133 targetRoleMarket: MarketSalary; 134 growthPotential: string; 135 bestMonetaryMove: string; 136 negotiationTips: string[]; 137 dataSource?: SalaryDataSource; 138 } 139 140 export interface MarketSalary { 141 low: number; 142 mid: number; 143 high: number; 144 currency: string; 145 region: string; 146 source?: SalaryDataSource; 147 } 148 149 export interface MissingSkill { 150 skill: string; 151 /** 152 * "important": core requirement, must-have 153 * "not_a_deal_breaker": preferred, learnable on the job 154 * "quick_win": learnable in 1 week (tool/platform/syntax gap, not structural) 155 * "unimportant": tangential mention, easily substituted 156 */ 157 importance: 'important' | 'not_a_deal_breaker' | 'quick_win' | 'unimportant'; 158 } 159 160 export interface JobMatch { 161 matchScore: number; 162 matchingSkills: string[]; 163 missingSkills: MissingSkill[]; 164 cvSuggestions: CVSuggestion[]; 165 overallAdvice: string; 166 /** 1-2 bullets: what this candidate brings that most applicants won't */ 167 competitiveAdvantage?: string[]; 168 } 169 170 export interface CVSuggestion { 171 section: string; 172 current: string; 173 suggested: string; 174 reasoning: string; 175 } 176 177 // --- Interview Prep Types --- 178 179 export interface InterviewPrep { 180 difficultyRating: 'easy' | 'moderate' | 'competitive' | 'highly_competitive'; 181 difficultyRationale: string; 182 focusAreas: string[]; 183 estimatedPrepHours: number; 184 technicalQuestions: TechnicalQuestion[]; 185 behavioralQuestions: BehavioralQuestion[]; 186 situationalQuestions: SituationalQuestion[]; 187 cultureQuestions: CultureQuestion[]; 188 technicalReview: TechnicalReviewItem[]; 189 softSkills: SoftSkillPrep[]; 190 questionsToAsk: QuestionToAsk[]; 191 mentalReadinessTip: string; 192 testGorillaTests: string[]; 193 } 194 195 export interface TechnicalQuestion { 196 question: string; 197 difficulty: 'easy' | 'medium' | 'hard'; 198 likelihood: 'very_likely' | 'possible'; 199 testing: string; 200 approach: string; 201 yourEdge?: string; 202 } 203 204 export interface BehavioralQuestion { 205 question: string; 206 competency: string; 207 starHints: { 208 situation: string; 209 task: string; 210 action: string; 211 result: string; 212 }; 213 } 214 215 export interface SituationalQuestion { 216 question: string; 217 framework: string; 218 } 219 220 export interface CultureQuestion { 221 question: string; 222 suggestedAngle: string; 223 } 224 225 export interface TechnicalReviewItem { 226 topic: string; 227 cluster: string; 228 whyItMatters: string; 229 whatToReview: string; 230 estimatedTime: '30min' | '1-2h' | 'half-day'; 231 searchTerm: string; 232 } 233 234 export interface SoftSkillPrep { 235 skill: string; 236 inContext: string; 237 howToSignal: string[]; 238 redFlag: string; 239 } 240 241 export interface QuestionToAsk { 242 question: string; 243 category: 'role' | 'team' | 'company' | 'growth' | 'strategic'; 244 } 245 246 // --- Intermediate Types (used between Claude calls) --- 247 248 export interface ExtractedProfile { 249 name: string; 250 currentRole: string; 251 totalYearsExperience: number; 252 skills: SkillCategory[]; 253 certifications: string[]; 254 education: EducationItem[]; 255 experience: ExperienceItem[]; 256 languages: LanguageItem[]; 257 summary: string; 258 } 259 260 export interface SkillCategory { 261 category: string; 262 skills: string[]; 263 proficiencyLevel: 'expert' | 'advanced' | 'intermediate' | 'beginner'; 264 } 265 266 export interface EducationItem { 267 degree: string; 268 institution: string; 269 year?: string; 270 field: string; 271 } 272 273 export interface ExperienceItem { 274 title: string; 275 company: string; 276 duration: string; 277 highlights: string[]; 278 technologies: string[]; 279 } 280 281 export interface LanguageItem { 282 language: string; 283 level: string; 284 } 285 286 // --- Career Profile Types (persistent user data) --- 287 288 export interface CareerProfile { 289 userId: string; 290 currentRole: string | null; 291 targetRole: string | null; 292 yearsExperience: number | null; 293 country: string | null; 294 workPreference: 'remote' | 'hybrid' | 'onsite' | 'flexible' | null; 295 githubUrl: string | null; 296 cvStoragePath: string | null; 297 linkedinStoragePath: string | null; 298 cvFilename: string | null; 299 linkedinFilename: string | null; 300 extractedProfile: ExtractedProfile | null; 301 additionalContext: string | null; 302 createdAt: string; 303 updatedAt: string; 304 } 305 306 export interface CareerProfileInput { 307 currentRole?: string; 308 targetRole?: string; 309 yearsExperience?: number; 310 country?: string; 311 workPreference?: 'remote' | 'hybrid' | 'onsite' | 'flexible'; 312 githubUrl?: string; 313 cvFilename?: string; 314 linkedinFilename?: string; 315 extractedProfile?: ExtractedProfile; 316 additionalContext?: string; 317 } 318 319 // --- API Request/Response Types --- 320 321 export interface AnalyzeRequest { 322 cv: File; 323 questionnaire: CareerQuestionnaire; 324 } 325 326 export interface MatchJobRequest { 327 cvText: string; 328 skills: string[]; 329 jobPosting: string; 330 } 331 332 export interface RewriteCVRequest { 333 cvText: string; 334 targetRole: string; 335 gaps: Gap[]; 336 jobPosting?: string; 337 } 338 339 export interface RewriteCVResponse { 340 suggestions: CVSuggestion[]; 341 rewrittenSummary: string; 342 } 343 344 // --- Error Types --- 345 346 export interface APIError { 347 error: string; 348 message: string; 349 status: number; 350 } 351 352 // --- Salary Data Source Types --- 353 354 export type SalaryDataSource = 'government_bls' | 'government_ons' | 'government_eurostat' | 'survey_stackoverflow' | 'market' | 'estimate'; 355 356 // --- Utility Types --- 357 358 export type SeverityColor = { 359 critical: string; 360 moderate: string; 361 minor: string; 362 }; 363 364 export type TierColor = { 365 differentiator: string; 366 strong: string; 367 supporting: string; 368 }; 369 370 // --- CV Generator types --- 371 372 // --- ATS Scoring Types --- 373 374 export interface ATSScoreResult { 375 overallScore: number; // 0-100 376 keywordScore: number; // 0-100 (weighted: required keywords count more) 377 formatScore: number; // 0-100 378 keywords: ATSKeywordAnalysis; 379 formatIssues: ATSFormatIssue[]; 380 recommendations: ATSRecommendation[]; 381 companyATS?: CompanyATSInfo; 382 } 383 384 export interface ATSKeywordAnalysis { 385 matched: ATSKeyword[]; 386 semanticMatch: ATSKeyword[]; // e.g., "React" ≈ "React.js" 387 missing: ATSKeyword[]; 388 total: { 389 required: number; 390 matched: number; 391 missing: number; 392 }; 393 } 394 395 export interface ATSKeyword { 396 keyword: string; 397 category: 'required' | 'preferred' | 'nice-to-have'; 398 matchedAs?: string; // what it matched in CV (for semantic matches) 399 cvSection?: string; // where found / where to add 400 importance: 'high' | 'medium' | 'low'; 401 } 402 403 export interface ATSFormatIssue { 404 issue: string; 405 severity: 'critical' | 'warning' | 'info'; 406 description: string; 407 fix: string; 408 } 409 410 export interface ATSRecommendation { 411 action: string; 412 section: string; // Which CV section to modify 413 priority: 'critical' | 'high' | 'medium'; 414 keywords: string[]; // Which keywords this addresses 415 example?: string; // Example of what to add/change 416 } 417 418 export interface CompanyATSInfo { 419 company: string; 420 atsSystem: string; // "Workday" | "Greenhouse" | "Lever" | "Taleo" | "iCIMS" | "SmartRecruiters" | "Unknown" 421 tips: string[]; 422 } 423 424 // --- ATS Score API Request/Response --- 425 426 export interface ATSScoreRequest { 427 cvText: string; 428 jobPosting: string; 429 extractedSkills?: string[]; 430 companyName?: string; 431 jobUrl?: string; 432 } 433 434 // --- Validation Types --- 435 436 export interface ValidationIssue { 437 section: string; // 'fitScore' | 'strengths' | 'gaps' | 'actionPlan' | 'salaryAnalysis' | 'roleRecommendations' | 'jobMatch' 438 severity: 'error' | 'warning' | 'info'; 439 field: string; // specific field path, e.g., 'fitScore.score', 'gaps[0].severity' 440 message: string; // human-readable issue description 441 autoFixable: boolean; // can the system fix this automatically? 442 autoFixAction?: string; // description of what auto-fix would do 443 } 444 445 export interface ValidationReport { 446 isValid: boolean; // true if no errors (warnings OK) 447 issues: ValidationIssue[]; 448 autoFixed: number; // count of issues auto-fixed 449 sections: Record<string, { valid: boolean; issueCount: number }>; 450 autoFixDescriptions: string[]; // human-readable descriptions of each auto-fix applied 451 } 452 453 // --- CV Generator types --- 454 455 export interface GeneratedCV { 456 professionalSummary: string; 457 skills: { 458 category: string; 459 items: string[]; 460 }[]; 461 experienceBullets: { 462 role: string; 463 company: string; 464 bullets: string[]; 465 }[]; 466 certifications: string[]; 467 projectHighlights: { 468 name: string; 469 description: string; 470 technologies: string[]; 471 }[]; 472 coverLetterDraft: string; 473 } 474 475 // --- Job Tracker Types --- 476 477 export type JobStatus = 'saved' | 'applied' | 'interviewing' | 'offer' | 'rejected' | 'withdrawn'; 478 479 export type JobSource = 'manual' | 'upwork' | 'linkedin'; 480 481 export interface JobApplication { 482 id: string; 483 userId: string; 484 company: string; 485 roleTitle: string; 486 jobUrl?: string; 487 jobPostingText?: string; 488 location?: string; 489 workType: 'remote' | 'hybrid' | 'onsite' | 'flexible'; 490 salaryMin?: number; 491 salaryMax?: number; 492 currency: string; 493 status: JobStatus; 494 statusUpdatedAt: string; 495 appliedAt?: string; 496 followUpAt?: string; 497 analysisId?: string; 498 matchScore?: number; 499 notes?: string; 500 contactName?: string; 501 contactEmail?: string; 502 source: JobSource; 503 metadata?: Record<string, unknown>; 504 sortOrder: number; 505 createdAt: string; 506 updatedAt: string; 507 } 508 509 export interface JobApplicationInput { 510 company: string; 511 roleTitle: string; 512 jobUrl?: string; 513 jobPostingText?: string; 514 location?: string; 515 workType?: 'remote' | 'hybrid' | 'onsite' | 'flexible'; 516 salaryMin?: number; 517 salaryMax?: number; 518 currency?: string; 519 status?: JobStatus; 520 appliedAt?: string; 521 followUpAt?: string; 522 notes?: string; 523 contactName?: string; 524 contactEmail?: string; 525 source?: JobSource; 526 metadata?: Record<string, unknown>; 527 } 528 529 export interface JobTrackerStats { 530 total: number; 531 byStatus: Record<JobStatus, number>; 532 avgMatchScore: number | null; 533 followUpsDue: number; 534 appliedThisWeek: number; 535 appliedThisMonth: number; 536 } 537 538 export interface KanbanColumn { 539 id: JobStatus; 540 label: string; 541 color: string; 542 icon: string; 543 } 544 545 // --- Quota & Subscription Types --- 546 547 export type QuotaType = 'analysis' | 'cv_generation' | 'cover_letter' | 'coach_request'; 548 549 export type PlanType = 'free' | 'pro'; 550 551 export type SubscriptionStatus = 'active' | 'past_due' | 'canceled' | 'trialing'; 552 553 export interface QuotaCheck { 554 allowed: boolean; 555 used: number; 556 limit: number; 557 plan: PlanType; 558 isInitialAnalysis?: boolean; 559 resetAt: string; // next Monday 00:00 UTC ISO string 560 } 561 562 export interface UserQuotaStatus { 563 plan: PlanType; 564 weekStart: string; 565 resetAt: string; 566 analysis: { used: number; limit: number }; 567 cvGeneration: { used: number; limit: number }; 568 coverLetter: { used: number; limit: number }; 569 coachRequest: { used: number; limit: number }; 570 hasUsedInitialAnalysis: boolean; 571 subscription: { 572 status: SubscriptionStatus | null; 573 periodEnd: string | null; 574 } | null; 575 } 576 577 export interface UserQuotaRow { 578 user_id: string; 579 plan: PlanType; 580 week_start: string; 581 analyses_used: number; 582 cv_generations_used: number; 583 cover_letters_used: number; 584 coach_requests_used: number; 585 analyses_limit: number; 586 cv_limit: number; 587 cover_letter_limit: number; 588 coach_limit: number; 589 has_used_initial_analysis: boolean; 590 stripe_customer_id: string | null; 591 stripe_subscription_id: string | null; 592 subscription_status: SubscriptionStatus | null; 593 subscription_period_end: string | null; 594 } 595 596 // --- Output Tag Types --- 597 598 export type OutputTagType = 'accurate' | 'inaccurate' | 'irrelevant' | 'missing_context' | 'too_generic'; 599 600 export interface OutputTag { 601 id: string; 602 userId: string; 603 analysisId: string; 604 section: string; 605 elementIndex: number | null; 606 elementKey: string | null; 607 taggedText: string | null; 608 tag: OutputTagType; 609 comment: string | null; 610 createdAt: string; 611 } 612 613 export interface OutputTagInput { 614 analysisId: string; 615 section: string; 616 elementIndex?: number; 617 elementKey?: string; 618 taggedText?: string; 619 tag: OutputTagType; 620 comment?: string; 621 } 622 623 // --- Upwork Types --- 624 625 export interface UpworkProfile { 626 name: string; 627 title: string; 628 overview: string; 629 hourlyRate?: number; 630 currency?: string; 631 totalEarnings?: number; 632 totalJobs?: number; 633 totalHours?: number; 634 jobSuccessScore?: number; 635 profileUrl?: string; 636 location?: string; 637 memberSince?: string; 638 skills: string[]; 639 categories: string[]; 640 employmentHistory: UpworkEmployment[]; 641 education: UpworkEducation[]; 642 certifications: string[]; 643 portfolio: UpworkPortfolioItem[]; 644 workHistory: UpworkJobHistory[]; 645 languages: Array<{ language: string; proficiency: string }>; 646 availability?: string; 647 responseTime?: string; 648 } 649 650 export interface UpworkEmployment { 651 title: string; 652 company: string; 653 startDate: string; 654 endDate?: string; 655 description: string; 656 } 657 658 export interface UpworkEducation { 659 degree: string; 660 institution: string; 661 year?: string; 662 } 663 664 export interface UpworkPortfolioItem { 665 title: string; 666 description: string; 667 url?: string; 668 } 669 670 export interface UpworkJobHistory { 671 title: string; 672 client?: string; 673 dateRange: string; 674 earnings?: number; 675 hours?: number; 676 feedback?: string; 677 rating?: number; 678 skills: string[]; 679 description: string; 680 } 681 682 export interface UpworkJobPosting { 683 title: string; 684 description: string; 685 clientInfo: { 686 country?: string; 687 paymentVerified?: boolean; 688 totalSpent?: string; 689 hireRate?: string; 690 totalJobs?: number; 691 avgHourlyRate?: string; 692 companySize?: string; 693 memberSince?: string; 694 rating?: number; 695 }; 696 budget: { 697 type: 'hourly' | 'fixed'; 698 min?: number; 699 max?: number; 700 currency?: string; 701 }; 702 skills: string[]; 703 experienceLevel?: 'entry' | 'intermediate' | 'expert'; 704 projectLength?: string; 705 weeklyHours?: string; 706 screeningQuestions: UpworkScreeningQuestion[]; 707 proposals?: number; 708 connects?: number; 709 postedDate?: string; 710 category?: string; 711 subcategory?: string; 712 } 713 714 export interface UpworkScreeningQuestion { 715 question: string; 716 type: 'text' | 'yesno' | 'choice' | 'attachment'; 717 required: boolean; 718 order: number; 719 options?: string[]; 720 maxLength?: number; 721 } 722 723 export interface UpworkCoverLetter { 724 openingHook: string; 725 screeningAnswers: UpworkScreeningAnswer[]; 726 body: string; 727 closingCta: string; 728 suggestedRate: { 729 amount: number; 730 type: 'hourly' | 'fixed'; 731 currency: string; 732 reasoning: string; 733 }; 734 profileOptimization: string[]; 735 } 736 737 export interface UpworkScreeningAnswer { 738 question: string; 739 answer: string; 740 order: number; 741 strategy: string; 742 } 743 744 export interface UpworkProfileAnalysis { 745 overallScore: number; 746 profileStrengths: Array<{ 747 area: string; 748 description: string; 749 impact: string; 750 }>; 751 profileWeaknesses: Array<{ 752 area: string; 753 description: string; 754 fix: string; 755 priority: 'critical' | 'high' | 'medium'; 756 }>; 757 titleOptimization: { 758 current: string; 759 suggested: string; 760 reasoning: string; 761 }; 762 overviewRewrite: { 763 current: string; 764 suggested: string; 765 reasoning: string; 766 }; 767 rateAdvice: { 768 currentRate?: number; 769 suggestedRange: { min: number; max: number }; 770 reasoning: string; 771 positioningStrategy: string; 772 }; 773 skillsAdvice: { 774 keep: string[]; 775 add: string[]; 776 remove: string[]; 777 reorder: string[]; 778 }; 779 nichingStrategy: string; 780 proposalTips: string[]; 781 competitivePosition: string; 782 }