From 93f5c9beb7f17abaf0fd7a3af305c8018ac21f3c Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 23 Mar 2026 22:34:50 +0100 Subject: [PATCH] Add AI draft variants for application package flows --- .../Controllers/JobApplicationsController.cs | 44 ++++++++++++++++++- .../src/components/JobDetailsDialog.tsx | 2 + job-tracker-ui/src/i18n/translations.ts | 4 ++ job-tracker-ui/src/types.ts | 2 + 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/JobTrackerApi/Controllers/JobApplicationsController.cs b/JobTrackerApi/Controllers/JobApplicationsController.cs index 9bd3718..aafb98b 100644 --- a/JobTrackerApi/Controllers/JobApplicationsController.cs +++ b/JobTrackerApi/Controllers/JobApplicationsController.cs @@ -186,6 +186,28 @@ namespace JobTrackerApi.Controllers }; } + private async Task> BuildDraftVariantsAsync(string baseInstruction, string context, CancellationToken cancellationToken, params string[] styles) + { + var variants = new List(); + + foreach (var style in styles.Where(x => !string.IsNullOrWhiteSpace(x))) + { + var draft = await _summarizer.SummarizeSectionAsync( + $"{baseInstruction} Style: {style}. Return only the final draft text.", + context, + 220, + 80); + + var normalized = draft?.Trim(); + if (!string.IsNullOrWhiteSpace(normalized) && !variants.Contains(normalized, StringComparer.OrdinalIgnoreCase)) + { + variants.Add(normalized); + } + } + + return variants; + } + private static List BuildFollowUpApproach(string status, List matchedTags, List missingTags) { var normalized = (status ?? string.Empty).Trim(); @@ -1442,7 +1464,7 @@ namespace JobTrackerApi.Controllers string? CoverLetterDraft, string? RecruiterMessageDraft); public sealed record SaveTailoredCvRequest(string? TailoredCvText); - public sealed record GenerateApplicationPackageDto(string TailoredCvText, string? CoverLetterDraft, string? ApplicationAnswerDraft, string? RecruiterMessageDraft, List KeyPoints, List AttachmentSignals, List AttachmentFilesUsed); + public sealed record GenerateApplicationPackageDto(string TailoredCvText, string? CoverLetterDraft, string? ApplicationAnswerDraft, string? RecruiterMessageDraft, List KeyPoints, List AttachmentSignals, List AttachmentFilesUsed, List CoverLetterVariants, List RecruiterMessageVariants); public sealed record SaveApplicationDraftsRequest(string? CoverLetterText, string? Notes, string? RecruiterMessageDraft); public sealed record InterviewPrepDto(string Summary, List TalkingPoints, List LikelyQuestions, List WeakSpots); public sealed record ReadinessDto(int Score, string Level, List Completed, List Missing, List Reminders); @@ -1830,12 +1852,28 @@ Candidate master CV: 170, 70); + var coverLetterVariants = await BuildDraftVariantsAsync( + "Write a concise, job-specific cover letter for this candidate and role. Use concrete evidence from the CV and job context, avoid generic enthusiasm, and keep the tone credible and polished.", + packageContext, + cancellationToken, + "concise and efficient", + "formal and polished", + "confident and high-conviction"); + var recruiterMessageDraft = await _summarizer.SummarizeSectionAsync( $"Write a short recruiter intro message for this candidate and role. Make it feel specific to the posting by mentioning the exact role, company, and one or two concrete overlaps from the candidate profile or job context. Keep it warm, direct, and concise. {packageModeInstruction}", packageContext, 140, 55); + var recruiterMessageVariants = await BuildDraftVariantsAsync( + "Write a short recruiter intro message for this candidate and role. Mention the exact role, company, and one or two concrete overlaps. Keep it natural, specific, and easy to respond to.", + packageContext, + cancellationToken, + "warm and conversational", + "direct and concise", + "polished and formal"); + var keyPoints = SkillTagger.Detect(jobText) .Distinct(StringComparer.OrdinalIgnoreCase) .Take(5) @@ -1849,7 +1887,9 @@ Candidate master CV: RecruiterMessageDraft: recruiterMessageDraft, KeyPoints: keyPoints, AttachmentSignals: attachmentContext?.Signals ?? new List(), - AttachmentFilesUsed: attachmentContext?.UsedFiles ?? new List())); + AttachmentFilesUsed: attachmentContext?.UsedFiles ?? new List(), + CoverLetterVariants: coverLetterVariants, + RecruiterMessageVariants: recruiterMessageVariants)); } [HttpGet("analytics-overview")] diff --git a/job-tracker-ui/src/components/JobDetailsDialog.tsx b/job-tracker-ui/src/components/JobDetailsDialog.tsx index 6c26149..f70f75f 100644 --- a/job-tracker-ui/src/components/JobDetailsDialog.tsx +++ b/job-tracker-ui/src/components/JobDetailsDialog.tsx @@ -437,6 +437,8 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0, } }} saving={savingApplicationDrafts} /> + 0 ? applicationPackage.coverLetterVariants : [t("jobDetailsNoDraftAvailable")]} /> + 0 ? applicationPackage.recruiterMessageVariants : [t("jobDetailsNoDraftAvailable")]} /> 0 ? applicationPackage.attachmentSignals : [t("jobDetailsNoAttachmentSignals")]} subtitle={applicationPackage.attachmentFilesUsed.length > 0 ? applicationPackage.attachmentFilesUsed.join(", ") : undefined} /> ) : null} diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index ec65913..ddeb6ee 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -711,6 +711,8 @@ export const translations = { jobDetailsRecruiterMessageSaved: "Recruiter message saved to this job.", jobDetailsRecruiterMessageSaveFailed: "Failed to save recruiter message.", jobDetailsKeyPoints: "Key points to emphasize", + jobDetailsCoverLetterVariants: "Cover letter variants", + jobDetailsRecruiterMessageVariants: "Recruiter message variants", jobDetailsAttachmentSignals: "Attachment-derived signals", jobDetailsNoAttachmentSignals: "No reusable attachment signals were found yet.", jobDetailsReason: "Reason", @@ -1487,6 +1489,8 @@ export const translations = { jobDetailsRecruiterMessageSaved: "Melding til rekrutterer lagret på denne jobben.", jobDetailsRecruiterMessageSaveFailed: "Kunne ikke lagre melding til rekrutterer.", jobDetailsKeyPoints: "Nøkkelpunkter å fremheve", + jobDetailsCoverLetterVariants: "Varianter av søknadsbrev", + jobDetailsRecruiterMessageVariants: "Varianter av meldinger til rekrutterer", jobDetailsAttachmentSignals: "Signal fra vedlegg", jobDetailsNoAttachmentSignals: "Ingen gjenbrukbare signaler fra vedlegg ble funnet ennå.", jobDetailsReason: "Årsak", diff --git a/job-tracker-ui/src/types.ts b/job-tracker-ui/src/types.ts index 8394b09..8341ff0 100644 --- a/job-tracker-ui/src/types.ts +++ b/job-tracker-ui/src/types.ts @@ -107,6 +107,8 @@ export interface ApplicationPackageResponse { keyPoints: string[]; attachmentSignals: string[]; attachmentFilesUsed: string[]; + coverLetterVariants: string[]; + recruiterMessageVariants: string[]; } export interface SaveApplicationDraftsRequest {