diff --git a/JobTrackerApi/Controllers/ProfileCvController.cs b/JobTrackerApi/Controllers/ProfileCvController.cs index 35363dd..65d50ba 100644 --- a/JobTrackerApi/Controllers/ProfileCvController.cs +++ b/JobTrackerApi/Controllers/ProfileCvController.cs @@ -109,6 +109,9 @@ public sealed class ProfileCvController : ControllerBase public JsonElement? JobApplicationId { get; set; } public string? TemplateId { get; set; } public string? SourceText { get; set; } + public string? PromptBackground { get; set; } + public string? Tone { get; set; } + public string? Language { get; set; } } public sealed record ParseCvRequest(string? Text); public sealed record CvTemplateDescriptor(string Id, string Title, string Tone, string AccentColor, string PreviewTagline, string PreviewSummary, List PreviewBullets); @@ -296,6 +299,9 @@ public sealed class ProfileCvController : ControllerBase var style = string.IsNullOrWhiteSpace(request.Style) ? "ats-minimal" : request.Style.Trim(); var templateId = NormalizeTemplateId(request.TemplateId ?? style); var targetRole = string.IsNullOrWhiteSpace(request.TargetRole) ? null : request.TargetRole.Trim(); + var tone = string.IsNullOrWhiteSpace(request.Tone) ? null : request.Tone.Trim(); + var language = string.IsNullOrWhiteSpace(request.Language) ? null : request.Language.Trim(); + var promptBackground = string.IsNullOrWhiteSpace(request.PromptBackground) ? null : request.PromptBackground.Trim(); var jobApplicationId = ParseFlexibleNullableInt(request.JobApplicationId); var jobContext = jobApplicationId.HasValue ? await _db.JobApplications @@ -327,9 +333,12 @@ public sealed class ProfileCvController : ControllerBase : effectiveTargetRole is not null ? $"Target role: {effectiveTargetRole}. Keep it broadly reusable but clearly aligned to that role family." : "Keep it broadly reusable for future tailoring."; + var toneGuidance = tone is not null ? $"Tone guidance: {tone}." : "Tone guidance: confident, professional, concise, and factual."; + var languageGuidance = language is not null ? $"Write the CV in {language}." : "Write the CV in English unless the source clearly requires another language."; + var backgroundGuidance = promptBackground is not null ? $"Candidate background and emphasis: {promptBackground}" : string.Empty; var subject = sectionName is null ? "this CV" : $"the '{sectionName}' section of this CV"; - var instruction = $"Rewrite only {subject}. Preserve facts, avoid inventing employers, titles, qualifications, dates, locations, or metrics. Style guidance: {style}. Template direction: {templateGuidance}. {roleGuidance} Return only the rewritten text with clean headings and bullets when useful."; + var instruction = $"Rewrite only {subject}. Preserve facts, avoid inventing employers, titles, qualifications, dates, locations, salaries, or metrics. Style guidance: {style}. Template direction: {templateGuidance}. {roleGuidance} {toneGuidance} {languageGuidance} {backgroundGuidance} Return only the rewritten CV text with clean headings and strong bullet phrasing when useful."; var rewritten = await _aiService.SummarizeSectionAsync( instruction, rewriteSource, diff --git a/job-tracker-ui/src/pages/ProfilePage.tsx b/job-tracker-ui/src/pages/ProfilePage.tsx index 47c3342..37ff125 100644 --- a/job-tracker-ui/src/pages/ProfilePage.tsx +++ b/job-tracker-ui/src/pages/ProfilePage.tsx @@ -28,6 +28,9 @@ import { JobApplication } from "../types"; type CvSectionOption = "" | "Professional Summary" | "Core Skills" | "Experience Highlights" | "Selected Achievements" | "Projects"; type CvSectionStyle = "ats-minimal" | "harvard" | "auckland" | "edinburgh" | "monarch" | "fjord"; +type CvBuilderTone = "Concise and direct" | "Executive and polished" | "Technical and detailed" | "Warm and people-focused"; +type CvBuilderLanguage = "English" | "Norwegian" | "Spanish" | "French" | "German"; + type ExtractionRun = { id: number; trigger: string; @@ -94,6 +97,9 @@ type RewriteRequestPayload = { targetRole: string | null; jobApplicationId: number | null; sourceText: string | null; + promptBackground: string | null; + tone: string | null; + language: string | null; }; type MeResponse = { @@ -242,6 +248,9 @@ export default function ProfilePage() { const [cvSection, setCvSection] = useState(""); const [cvSectionStyle, setCvSectionStyle] = useState("ats-minimal"); const [cvSectionTargetRole, setCvSectionTargetRole] = useState(""); + const [cvPromptBackground, setCvPromptBackground] = useState(""); + const [cvTone, setCvTone] = useState("Concise and direct"); + const [cvLanguage, setCvLanguage] = useState("English"); const [selectedRewriteJobId, setSelectedRewriteJobId] = useState(""); const [rewritePreview, setRewritePreview] = useState(null); const [rewritePreviewTemplate, setRewritePreviewTemplate] = useState(null); @@ -361,7 +370,10 @@ export default function ProfilePage() { targetRole: cvSectionTargetRole.trim() || null, jobApplicationId: selectedRewriteJob ? selectedRewriteJob.id : null, sourceText: profileCvText.trim() || null, - }), [cvSection, cvSectionTargetRole, profileCvText, selectedRewriteJob]); + promptBackground: cvPromptBackground.trim() || null, + tone: cvTone, + language: cvLanguage, + }), [cvLanguage, cvPromptBackground, cvSection, cvSectionTargetRole, cvTone, profileCvText, selectedRewriteJob]); const resetPdfCarousel = useCallback(() => { setPdfCarousel((current) => { @@ -1030,6 +1042,16 @@ export default function ProfilePage() { + setCvPromptBackground(e.target.value)} + fullWidth + multiline + minRows={4} + helperText="Describe your strengths, preferred emphasis, industry background, or the angle you want the AI to lean into." + sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }} + /> {t("profileCvSectionLabel")} setCvLanguage(e.target.value as CvBuilderLanguage)}> + English + Norwegian + Spanish + French + German + + + + Tone + + Saved job context