Add guided CV builder controls
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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))}>
|
||||
|
||||
@@ -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',
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user