diff --git a/Apis/api/Controllers/CvMatcherController.cs b/Apis/api/Controllers/CvMatcherController.cs index 03098f2..3b0bcc1 100644 --- a/Apis/api/Controllers/CvMatcherController.cs +++ b/Apis/api/Controllers/CvMatcherController.cs @@ -246,38 +246,24 @@ public sealed class CvMatcherController : ControllerBase { var result = await _jobSearchApi.StartSearchAsync(t, ct); var lang = "en"; - var html = result.Status switch + var (title, message) = result.Status switch { - StartJobSearchStatus.Started => - HtmlPage(_templates.Get("html.job-search.started.title", lang), _templates.Get("html.job-search.started.message", lang)), - StartJobSearchStatus.AlreadyUsed => - HtmlPage(_templates.Get("html.job-search.already-used.title", lang), _templates.Get("html.job-search.already-used.message", lang)), - StartJobSearchStatus.Expired => - HtmlPage(_templates.Get("html.job-search.expired.title", lang), _templates.Get("html.job-search.expired.message", lang)), - _ => - HtmlPage(_templates.Get("html.job-search.invalid.title", lang), _templates.Get("html.job-search.invalid.message", lang)) + StartJobSearchStatus.Started => (_templates.Get("html.job-search.started.title", lang), _templates.Get("html.job-search.started.message", lang)), + StartJobSearchStatus.AlreadyUsed => (_templates.Get("html.job-search.already-used.title", lang), _templates.Get("html.job-search.already-used.message", lang)), + StartJobSearchStatus.Expired => (_templates.Get("html.job-search.expired.title", lang), _templates.Get("html.job-search.expired.message", lang)), + _ => (_templates.Get("html.job-search.invalid.title", lang), _templates.Get("html.job-search.invalid.message", lang)) }; - return Content(html, "text/html"); + return Content(_templates.Render("html.job-search.shell", "*", ("title", title), ("message", message)), "text/html"); } catch (Exception ex) { _logger.LogError(ex, "Job search start failed for token {Token}.", t); - return Content(HtmlPage(_templates.Get("html.job-search.error.title", "en"), _templates.Get("html.job-search.error.message", "en")), "text/html"); + var title = _templates.Get("html.job-search.error.title", "en"); + var message = _templates.Get("html.job-search.error.message", "en"); + return Content(_templates.Render("html.job-search.shell", "*", ("title", title), ("message", message)), "text/html"); } } - private static string HtmlPage(string title, string message) => $$""" - - -
{{message}}
—
"; + : "—
"; var gaps = result.Gaps?.Count > 0 - ? "—
"; + : "—
"; var recommendations = result.Recommendations?.Count > 0 - ? "—
"; + : "—
"; + // Render the HTML template with substituted values + // email.match.body is now stored as HTML in the database var body = _emailTemplates.Render("email.match.body", language, ("cvDocumentId", cvDocumentId), ("jobLabel", jobLabel ?? "N/A"), ("jobUrl", result.JobUrl ?? "N/A"), ("score", result.Score.ToString()), - ("summary", result.Summary ?? string.Empty), + ("summary", WebUtility.HtmlEncode(result.Summary ?? string.Empty)), ("strengths", strengths), ("gaps", gaps), ("recommendations", recommendations)); + // Append the job search footer if link is provided if (!string.IsNullOrWhiteSpace(jobSearchLink)) { body += _emailTemplates.Render("email.match.job-search-footer", language, diff --git a/Apis/cv-matcher-data/Migrations/20260601133028_InitialSchema.cs b/Apis/cv-matcher-data/Migrations/20260601133028_InitialSchema.cs index 13e650c..78e799e 100644 --- a/Apis/cv-matcher-data/Migrations/20260601133028_InitialSchema.cs +++ b/Apis/cv-matcher-data/Migrations/20260601133028_InitialSchema.cs @@ -82,12 +82,12 @@ namespace CvMatcher.Data.Migrations // AI system prompt for CV matching — English Row("ai.cv-match.system-prompt", "en", - "You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100. Penalize missing required skills. Do not invent experience. Use concise business language. All text fields in the JSON response must be in English.\nJSON shape: {\"score\":number,\"summary\":\"one-line summary in English\",\"strengths\":[\"strength 1 in English\",\"strength 2 in English\"],\"gaps\":[\"gap 1 in English\"],\"recommendations\":[\"recommendation 1 in English\"],\"evidence\":[\"evidence 1 in English\"]}", + "You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100. Penalize missing required skills. Do not invent experience. Use concise business language. All text fields in the JSON response must be in English.\nJSON shape: {\"score\":number,\"summary\":\"one-line summary in English\",\"strengths\":[\"strength 1 in English\"],\"gaps\":[\"gap 1 in English\"],\"recommendations\":[\"recommendation 1 in English\"],\"evidence\":[\"evidence 1 in English\"],\"keywords\":[\"keyword1\",\"keyword2\",\"keyword3\"]}", "System prompt for CV-to-job matching in English. Instructs LLM to return JSON with CV strengths, gaps, and recommendations relative to the job."); // AI system prompt for CV matching — Romanian Row("ai.cv-match.system-prompt", "ro", - "Ești un motor strict de potrivire CV-job. Returnează doar JSON. Punctează realist între 0 și 100. Penalizează abilitățile lipsă necesare. Nu inventa experiență. Folosește limbaj profesional concis. Toate câmpurile text din răspunsul JSON trebuie să fie în limba română.\nJSON shape: {\"score\":number,\"summary\":\"rezumat pe o linie în română\",\"strengths\":[\"punct forte 1 în română\",\"punct forte 2 în română\"],\"gaps\":[\"lipsă 1 în română\"],\"recommendations\":[\"recomandare 1 în română\"],\"evidence\":[\"dovadă 1 în română\"]}", + "Ești un motor strict de potrivire CV-job. Returnează doar JSON. Punctează realist între 0 și 100. Penalizează abilitățile lipsă necesare. Nu inventa experiență. Folosește limbaj profesional concis. Toate câmpurile text din răspunsul JSON trebuie să fie în limba română.\nJSON shape: {\"score\":number,\"summary\":\"rezumat pe o linie în română\",\"strengths\":[\"punct forte 1 în română\"],\"gaps\":[\"lipsă 1 în română\"],\"recommendations\":[\"recomandare 1 în română\"],\"evidence\":[\"dovadă 1 în română\"],\"keywords\":[\"cuvant1\",\"cuvant2\",\"cuvant3\"]}", "System prompt pentru potrivire CV-job în limba română. Instruiește LLM-ul să returneze JSON cu punctele forte ale CV-ului, lacunele și recomandări relative la job."); } diff --git a/Apis/email-data/Migrations/20260601133043_InitialSchema.cs b/Apis/email-data/Migrations/20260601133043_InitialSchema.cs index 0568ed2..98a2518 100644 --- a/Apis/email-data/Migrations/20260601133043_InitialSchema.cs +++ b/Apis/email-data/Migrations/20260601133043_InitialSchema.cs @@ -44,59 +44,180 @@ namespace Email.Data.Migrations Row("email.match.subject", "en", "MyAi.ro CV Match: {{score}}% - {{jobLabel}}", "Subject for the CV match result email"); Row("email.match.subject", "ro", "MyAi.ro Potrivire CV: {{score}}% - {{jobLabel}}", "Subiect email rezultat potrivire CV"); - // Match result email — body + // Match result email — body (HTML formatted) Row("email.match.body", "en", - "CV Matcher result\n\nCV Document ID: {{cvDocumentId}}\nJob: {{jobLabel}}\nJob URL: {{jobUrl}}\nScore: {{score}}%\n\nSummary:\n{{summary}}\n\nStrengths:\n{{strengths}}\n\nGaps:\n{{gaps}}\n\nRecommendations:\n{{recommendations}}", - "Body for the CV match result email"); + @"| CV ID | +{{cvDocumentId}} | +
| Job | +{{jobLabel}} | +
| URL | +{{jobUrl}} | +
| Score | +{{score}}% | +
{{summary}}
+| ID Document CV | +{{cvDocumentId}} | +
| Job | +{{jobLabel}} | +
| URL | +{{jobUrl}} | +
| Scor | +{{score}}% | +
{{summary}}
+Want to find more jobs matching your CV?
+| + Search Jobs + | +
(link valid for {{expiryDays}} days)
", + "Job search CTA appended to match result email (HTML formatted)"); Row("email.match.job-search-footer", "ro", - "\n\n---\nVrei sa gasesti mai multe joburi potrivite CV-ului tau?\nClick: {{jobSearchLink}}\n(link valabil {{expiryDays}} zile)", - "CTA cautare joburi adaugat la emailul de potrivire CV"); + @"Vrei să găsești mai multe joburi potrivite CV-ului tău?
+| + Caută Joburi + | +
(link valabil {{expiryDays}} zile)
", + "CTA cautare joburi adaugat la emailul de potrivire CV (format HTML)"); // Job search results email — subject Row("email.search-results.subject", "en", "MyAi.ro: {{count}} jobs matching your CV", "Subject for job search results email"); Row("email.search-results.subject", "ro", "MyAi.ro: {{count}} joburi potrivite CV-ului tau", "Subiect email rezultate cautare joburi"); - // Job search results email — body preamble (items appended in code) - Row("email.search-results.body", "en", "MyAi.ro found {{count}} jobs matching your CV:\n\n{{items}}", "Body preamble for job search results email"); - Row("email.search-results.body", "ro", "MyAi.ro a gasit {{count}} joburi potrivite CV-ului tau:\n\n{{items}}", "Corpul emailului de rezultate cautare joburi"); + // Job search results email — body preamble (items appended in code) - HTML formatted + Row("email.search-results.body", "en", + @"MyAi.ro found {{count}} jobs matching your CV:
+{{items}}", + "Body preamble for job search results email (HTML formatted)"); + Row("email.search-results.body", "ro", + @"MyAi.ro a găsit {{count}} joburi potrivite CV-ului tău:
+{{items}}", + "Corpul emailului de rezultate cautare joburi (format HTML)"); - // Job search results email — no results found - Row("email.search-results.empty", "en", "MyAi.ro found no jobs matching your CV. Try again later or update your CV.", "No results message for job search results email"); - Row("email.search-results.empty", "ro", "MyAi.ro nu a gasit joburi care sa corespunda CV-ului tau. Incercati mai tarziu sau ajustati CV-ul.", "Mesaj fara rezultate pentru emailul de cautare joburi"); + // Job search results email — scan summary block (keywords + providers used) + Row("email.search-results.scan-summary", "en", + @"|
+ Keywords used: {{keywordsHtml}}
+ Providers scanned: {{providers}}
+ |
+
|
+ Cuvinte cheie folosite: {{keywordsHtml}}
+ Furnizori scanați: {{providers}}
+ |
+
|
+ {{index}}. {{jobTitle}}
+ {{score}}% match
+ [{{providerName}}] + {{jobUrl}} + {{summary}} + |
+
|
+ {{index}}. {{jobTitle}}
+ {{score}}% potrivire
+ [{{providerName}}] + {{jobUrl}} + {{summary}} + |
+
|
+ No jobs found |
+
|
+ Niciun job găsit |
+
\n
| \n
{{message}}
{{message}}
{summary}
"; - items.Append($""" -{summary}
")} -