Add guided CV builder controls
CI and Deploy / test (push) Failing after 0s
CI and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-04-20 21:22:31 +02:00
parent eea327e1f6
commit 657cb95a48
3 changed files with 59 additions and 6 deletions
@@ -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<string> 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,
+42 -1
View File
@@ -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<CvSectionOption>("");
const [cvSectionStyle, setCvSectionStyle] = useState<CvSectionStyle>("ats-minimal");
const [cvSectionTargetRole, setCvSectionTargetRole] = useState("");
const [cvPromptBackground, setCvPromptBackground] = useState("");
const [cvTone, setCvTone] = useState<CvBuilderTone>("Concise and direct");
const [cvLanguage, setCvLanguage] = useState<CvBuilderLanguage>("English");
const [selectedRewriteJobId, setSelectedRewriteJobId] = useState<string>("");
const [rewritePreview, setRewritePreview] = useState<CvBuilderPreview | null>(null);
const [rewritePreviewTemplate, setRewritePreviewTemplate] = useState<RewriteTemplateOption | null>(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() {
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5, mb: 1.75 }}>
<TextField
label="Prompt-based CV brief"
value={cvPromptBackground}
onChange={(e) => 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" } }}
/>
<FormControl fullWidth size="small">
<InputLabel>{t("profileCvSectionLabel")}</InputLabel>
<Select value={cvSection} label={t("profileCvSectionLabel")} onChange={(e) => setCvSection(e.target.value as CvSectionOption)}>
@@ -1048,6 +1070,25 @@ export default function ProfilePage() {
fullWidth
helperText={selectedRewriteJob ? `Using saved job context: ${selectedRewriteJob.jobTitle}` : "Leave empty to let the selected job drive tailoring."}
/>
<FormControl fullWidth size="small">
<InputLabel>Language</InputLabel>
<Select value={cvLanguage} label="Language" onChange={(e) => setCvLanguage(e.target.value as CvBuilderLanguage)}>
<MenuItem value="English">English</MenuItem>
<MenuItem value="Norwegian">Norwegian</MenuItem>
<MenuItem value="Spanish">Spanish</MenuItem>
<MenuItem value="French">French</MenuItem>
<MenuItem value="German">German</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth size="small">
<InputLabel>Tone</InputLabel>
<Select value={cvTone} label="Tone" onChange={(e) => setCvTone(e.target.value as CvBuilderTone)}>
<MenuItem value="Concise and direct">Concise and direct</MenuItem>
<MenuItem value="Executive and polished">Executive and polished</MenuItem>
<MenuItem value="Technical and detailed">Technical and detailed</MenuItem>
<MenuItem value="Warm and people-focused">Warm and people-focused</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth size="small" sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }}>
<InputLabel>Saved job context</InputLabel>
<Select value={selectedRewriteJobId} label="Saved job context" onChange={(e) => setSelectedRewriteJobId(String(e.target.value))}>
+7 -4
View File
@@ -248,9 +248,8 @@ test('profile page rewrite tools use selected template and saved job context', a
expect(await screen.findByText(/template-driven cv builder/i)).toBeInTheDocument();
fireEvent.click(screen.getByText(/harvard/i));
fireEvent.mouseDown(screen.getAllByRole('combobox')[1]);
fireEvent.click(await screen.findByText(/senior backend engineer · acme systems/i));
fireEvent.change(screen.getByLabelText(/prompt-based cv brief/i), { target: { value: 'Highlight backend platform ownership, distributed systems, and cross-team delivery.' } });
fireEvent.change(screen.getByLabelText(/target role/i), { target: { value: 'Senior Platform Engineer' } });
const rewriteButton = screen.getByRole('button', { name: /build preview/i });
fireEvent.click(rewriteButton);
@@ -259,7 +258,11 @@ test('profile page rewrite tools use selected template and saved job context', a
sectionName: null,
style: 'harvard',
templateId: 'harvard',
jobApplicationId: 42,
jobApplicationId: null,
promptBackground: 'Highlight backend platform ownership, distributed systems, and cross-team delivery.',
targetRole: 'Senior Platform Engineer',
language: 'English',
tone: 'Concise and direct',
}));
});