Add AI draft variants for application package flows

This commit is contained in:
cesnimda
2026-03-23 22:34:50 +01:00
parent 05bc42c3d5
commit 93f5c9beb7
4 changed files with 50 additions and 2 deletions
@@ -186,6 +186,28 @@ namespace JobTrackerApi.Controllers
};
}
private async Task<List<string>> BuildDraftVariantsAsync(string baseInstruction, string context, CancellationToken cancellationToken, params string[] styles)
{
var variants = new List<string>();
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<string> BuildFollowUpApproach(string status, List<string> matchedTags, List<string> 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<string> KeyPoints, List<string> AttachmentSignals, List<string> AttachmentFilesUsed);
public sealed record GenerateApplicationPackageDto(string TailoredCvText, string? CoverLetterDraft, string? ApplicationAnswerDraft, string? RecruiterMessageDraft, List<string> KeyPoints, List<string> AttachmentSignals, List<string> AttachmentFilesUsed, List<string> CoverLetterVariants, List<string> RecruiterMessageVariants);
public sealed record SaveApplicationDraftsRequest(string? CoverLetterText, string? Notes, string? RecruiterMessageDraft);
public sealed record InterviewPrepDto(string Summary, List<string> TalkingPoints, List<string> LikelyQuestions, List<string> WeakSpots);
public sealed record ReadinessDto(int Score, string Level, List<string> Completed, List<string> Missing, List<string> 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<string>(),
AttachmentFilesUsed: attachmentContext?.UsedFiles ?? new List<string>()));
AttachmentFilesUsed: attachmentContext?.UsedFiles ?? new List<string>(),
CoverLetterVariants: coverLetterVariants,
RecruiterMessageVariants: recruiterMessageVariants));
}
[HttpGet("analytics-overview")]
@@ -437,6 +437,8 @@ export default function JobDetailsDialog({ open, jobId, onClose, initialTab = 0,
}
}} saving={savingApplicationDrafts} />
<ListCard title={t("jobDetailsKeyPoints")} items={applicationPackage.keyPoints} />
<ListCard title={t("jobDetailsCoverLetterVariants")} items={applicationPackage.coverLetterVariants.length > 0 ? applicationPackage.coverLetterVariants : [t("jobDetailsNoDraftAvailable")]} />
<ListCard title={t("jobDetailsRecruiterMessageVariants")} items={applicationPackage.recruiterMessageVariants.length > 0 ? applicationPackage.recruiterMessageVariants : [t("jobDetailsNoDraftAvailable")]} />
<ListCard title={t("jobDetailsAttachmentSignals")} items={applicationPackage.attachmentSignals.length > 0 ? applicationPackage.attachmentSignals : [t("jobDetailsNoAttachmentSignals")]} subtitle={applicationPackage.attachmentFilesUsed.length > 0 ? applicationPackage.attachmentFilesUsed.join(", ") : undefined} />
</Box>
) : null}
+4
View File
@@ -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",
+2
View File
@@ -107,6 +107,8 @@ export interface ApplicationPackageResponse {
keyPoints: string[];
attachmentSignals: string[];
attachmentFilesUsed: string[];
coverLetterVariants: string[];
recruiterMessageVariants: string[];
}
export interface SaveApplicationDraftsRequest {