feat: DB-backed localized templates + language-aware emails #15
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using Shared.Models.Responses;
|
using Shared.Models.Responses;
|
||||||
|
using MyAi.Models.Services;
|
||||||
|
|
||||||
namespace Api.Controllers;
|
namespace Api.Controllers;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
private readonly FileStorageSettings _fileStorageSettings;
|
private readonly FileStorageSettings _fileStorageSettings;
|
||||||
private readonly JobSearchLinkSettings _jobSearchLinkSettings;
|
private readonly JobSearchLinkSettings _jobSearchLinkSettings;
|
||||||
private readonly IEmailSender _emailSender;
|
private readonly IEmailSender _emailSender;
|
||||||
|
private readonly ITemplateService _templates;
|
||||||
private readonly ILogger<CvMatcherController> _logger;
|
private readonly ILogger<CvMatcherController> _logger;
|
||||||
|
|
||||||
public CvMatcherController(
|
public CvMatcherController(
|
||||||
@@ -36,6 +38,7 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
IOptions<FileStorageSettings> fileStorageSettings,
|
IOptions<FileStorageSettings> fileStorageSettings,
|
||||||
IOptions<JobSearchLinkSettings> jobSearchLinkSettings,
|
IOptions<JobSearchLinkSettings> jobSearchLinkSettings,
|
||||||
IEmailSender emailSender,
|
IEmailSender emailSender,
|
||||||
|
ITemplateService templates,
|
||||||
ILogger<CvMatcherController> logger)
|
ILogger<CvMatcherController> logger)
|
||||||
{
|
{
|
||||||
_cvApi = cvApi;
|
_cvApi = cvApi;
|
||||||
@@ -44,6 +47,7 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
_fileStorageSettings = fileStorageSettings.Value;
|
_fileStorageSettings = fileStorageSettings.Value;
|
||||||
_jobSearchLinkSettings = jobSearchLinkSettings.Value;
|
_jobSearchLinkSettings = jobSearchLinkSettings.Value;
|
||||||
_emailSender = emailSender;
|
_emailSender = emailSender;
|
||||||
|
_templates = templates;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,13 +164,15 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
? request.JobUrl
|
? request.JobUrl
|
||||||
: "Manual job description";
|
: "Manual job description";
|
||||||
|
|
||||||
|
var language = NormalizeLanguage(request.Language);
|
||||||
|
|
||||||
string? jobSearchLink = null;
|
string? jobSearchLink = null;
|
||||||
if (!string.IsNullOrWhiteSpace(request.Email) && !string.IsNullOrWhiteSpace(request.CvDocumentId))
|
if (!string.IsNullOrWhiteSpace(request.Email) && !string.IsNullOrWhiteSpace(request.CvDocumentId))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenResp = await _jobSearchApi.CreateTokenAsync(
|
var tokenResp = await _jobSearchApi.CreateTokenAsync(
|
||||||
new CreateJobSearchTokenRequest { CvDocumentId = request.CvDocumentId, Email = request.Email },
|
new CreateJobSearchTokenRequest { CvDocumentId = request.CvDocumentId, Email = request.Email, Language = language },
|
||||||
ct);
|
ct);
|
||||||
var baseUrl = _jobSearchLinkSettings.BaseUrl.TrimEnd('/');
|
var baseUrl = _jobSearchLinkSettings.BaseUrl.TrimEnd('/');
|
||||||
jobSearchLink = $"{baseUrl}/api/cv-matcher/job-search/start?t={tokenResp.TokenId}";
|
jobSearchLink = $"{baseUrl}/api/cv-matcher/job-search/start?t={tokenResp.TokenId}";
|
||||||
@@ -179,8 +185,8 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
|
|
||||||
await _emailSender.SendMatchAsync(
|
await _emailSender.SendMatchAsync(
|
||||||
request.Email,
|
request.Email,
|
||||||
SmtpEmailSender.BuildMatchEmailSubject(res.Score, jobLabel),
|
_emailSender.BuildMatchEmailSubject(res.Score, jobLabel, language),
|
||||||
SmtpEmailSender.BuildMatchEmailBody(request.CvDocumentId ?? "N/A", res, jobLabel, jobSearchLink),
|
_emailSender.BuildMatchEmailBody(request.CvDocumentId ?? "N/A", res, jobLabel, language, jobSearchLink),
|
||||||
attachmentPath,
|
attachmentPath,
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
@@ -217,23 +223,24 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _jobSearchApi.StartSearchAsync(t, ct);
|
var result = await _jobSearchApi.StartSearchAsync(t, ct);
|
||||||
|
var lang = "en";
|
||||||
var html = result.Status switch
|
var html = result.Status switch
|
||||||
{
|
{
|
||||||
StartJobSearchStatus.Started =>
|
StartJobSearchStatus.Started =>
|
||||||
HtmlPage("Job search started", "Your job search has started. Results will be sent to your email shortly."),
|
HtmlPage(_templates.Get("html.job-search.started.title", lang), _templates.Get("html.job-search.started.message", lang)),
|
||||||
StartJobSearchStatus.AlreadyUsed =>
|
StartJobSearchStatus.AlreadyUsed =>
|
||||||
HtmlPage("Link already used", "This job search link has already been used."),
|
HtmlPage(_templates.Get("html.job-search.already-used.title", lang), _templates.Get("html.job-search.already-used.message", lang)),
|
||||||
StartJobSearchStatus.Expired =>
|
StartJobSearchStatus.Expired =>
|
||||||
HtmlPage("Link expired", "This job search link has expired. Please request a new CV match to get a fresh link."),
|
HtmlPage(_templates.Get("html.job-search.expired.title", lang), _templates.Get("html.job-search.expired.message", lang)),
|
||||||
_ =>
|
_ =>
|
||||||
HtmlPage("Invalid link", "This job search link is not valid.")
|
HtmlPage(_templates.Get("html.job-search.invalid.title", lang), _templates.Get("html.job-search.invalid.message", lang))
|
||||||
};
|
};
|
||||||
return Content(html, "text/html");
|
return Content(html, "text/html");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Job search start failed for token {Token}.", t);
|
_logger.LogError(ex, "Job search start failed for token {Token}.", t);
|
||||||
return Content(HtmlPage("Error", "An error occurred. Please try again later."), "text/html");
|
return Content(HtmlPage(_templates.Get("html.job-search.error.title", "en"), _templates.Get("html.job-search.error.message", "en")), "text/html");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +295,9 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
return Path.Combine(GetFileStoragePath(), $"{safeId}.pdf");
|
return Path.Combine(GetFileStoragePath(), $"{safeId}.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeLanguage(string? language) =>
|
||||||
|
string.IsNullOrWhiteSpace(language) ? "en" : language.ToLowerInvariant().Split('-')[0].Trim();
|
||||||
|
|
||||||
private string GetFileStoragePath()
|
private string GetFileStoragePath()
|
||||||
{
|
{
|
||||||
var fileStoragePath = _fileStorageSettings.Path;
|
var fileStoragePath = _fileStorageSettings.Path;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ COPY Apis/api/api.csproj Apis/api/
|
|||||||
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
|
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
|
||||||
COPY Apis/api-models/api-models.csproj Apis/api-models/
|
COPY Apis/api-models/api-models.csproj Apis/api-models/
|
||||||
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
|
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
|
||||||
|
COPY Apis/myai-models/myai-models.csproj Apis/myai-models/
|
||||||
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
|
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
|
||||||
|
|
||||||
RUN dotnet restore Apis/api/api.csproj
|
RUN dotnet restore Apis/api/api.csproj
|
||||||
@@ -14,6 +15,7 @@ COPY Apis/api/ Apis/api/
|
|||||||
COPY Apis/shared-models/ Apis/shared-models/
|
COPY Apis/shared-models/ Apis/shared-models/
|
||||||
COPY Apis/api-models/ Apis/api-models/
|
COPY Apis/api-models/ Apis/api-models/
|
||||||
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
|
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
|
||||||
|
COPY Apis/myai-models/ Apis/myai-models/
|
||||||
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
|
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
|
||||||
|
|
||||||
RUN dotnet publish Apis/api/api.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
RUN dotnet publish Apis/api/api.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Api.Services;
|
using Api.Services;
|
||||||
using Api.Services.Contracts;
|
using Api.Services.Contracts;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Models.Settings;
|
using Models.Settings;
|
||||||
|
using MyAi.Models.Data;
|
||||||
|
using MyAi.Models.Services;
|
||||||
using Refit;
|
using Refit;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Shared.Models.Settings;
|
||||||
using StartupHelpers;
|
using StartupHelpers;
|
||||||
|
|
||||||
StartupExtensions.LoadDotEnvFile();
|
StartupExtensions.LoadDotEnvFile();
|
||||||
@@ -30,6 +34,17 @@ try
|
|||||||
builder.Services.Configure<FileStorageSettings>(builder.Configuration.GetSection("FileStorage"));
|
builder.Services.Configure<FileStorageSettings>(builder.Configuration.GetSection("FileStorage"));
|
||||||
builder.Services.Configure<JobSearchLinkSettings>(builder.Configuration.GetSection("JobSearch"));
|
builder.Services.Configure<JobSearchLinkSettings>(builder.Configuration.GetSection("JobSearch"));
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<MyAiDbContext>(options =>
|
||||||
|
{
|
||||||
|
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
|
||||||
|
options.UseSqlServer(connectionString, sql =>
|
||||||
|
{
|
||||||
|
sql.MigrationsAssembly("myai-models");
|
||||||
|
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<ITemplateService, DbTemplateService>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient<ICaptchaVerifier, RecaptchaVerifier>();
|
builder.Services.AddHttpClient<ICaptchaVerifier, RecaptchaVerifier>();
|
||||||
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
|
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
|
||||||
builder.Services.AddSingleton<Microsoft.AspNetCore.StaticFiles.IContentTypeProvider, Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider>();
|
builder.Services.AddSingleton<Microsoft.AspNetCore.StaticFiles.IContentTypeProvider, Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider>();
|
||||||
@@ -71,6 +86,13 @@ try
|
|||||||
app.UseRateLimiter();
|
app.UseRateLimiter();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
Log.Information("Running EF Core migrations if any");
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
Log.Information("{Service} startup complete. Listening for requests...", ServiceName);
|
Log.Information("{Service} startup complete. Listening for requests...", ServiceName);
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Models.Requests;
|
using CvMatcher.Models.Responses;
|
||||||
|
using Models.Requests;
|
||||||
|
|
||||||
namespace Api.Services.Contracts
|
namespace Api.Services.Contracts
|
||||||
{
|
{
|
||||||
@@ -8,5 +9,7 @@ namespace Api.Services.Contracts
|
|||||||
Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct);
|
Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct);
|
||||||
Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct);
|
Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct);
|
||||||
Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct);
|
Task SendMatchAsync(string? explicitTo, string subject, string body, string? attachmentPath, CancellationToken ct);
|
||||||
|
string BuildMatchEmailSubject(int score, string? jobLabel, string language);
|
||||||
|
string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using MimeKit;
|
|||||||
using Models.Settings;
|
using Models.Settings;
|
||||||
using Models.Requests;
|
using Models.Requests;
|
||||||
using CvMatcher.Models.Responses;
|
using CvMatcher.Models.Responses;
|
||||||
|
using MyAi.Models.Services;
|
||||||
|
|
||||||
namespace Api.Services
|
namespace Api.Services
|
||||||
{
|
{
|
||||||
@@ -15,27 +16,29 @@ namespace Api.Services
|
|||||||
private readonly ContactSettings _contact;
|
private readonly ContactSettings _contact;
|
||||||
private readonly SubscribeSettings _subscribe;
|
private readonly SubscribeSettings _subscribe;
|
||||||
private readonly FileStorageSettings _fileStorage;
|
private readonly FileStorageSettings _fileStorage;
|
||||||
|
private readonly ITemplateService _templates;
|
||||||
private readonly ILogger<SmtpEmailSender> _log;
|
private readonly ILogger<SmtpEmailSender> _log;
|
||||||
private readonly string _environmentName;
|
private readonly string _environmentName;
|
||||||
|
|
||||||
public SmtpEmailSender(IOptions<SmtpSettings> smtp,
|
public SmtpEmailSender(
|
||||||
|
IOptions<SmtpSettings> smtp,
|
||||||
IOptions<ContactSettings> contact,
|
IOptions<ContactSettings> contact,
|
||||||
IOptions<SubscribeSettings> subscribe,
|
IOptions<SubscribeSettings> subscribe,
|
||||||
IOptions<FileStorageSettings> fileStorage,
|
IOptions<FileStorageSettings> fileStorage,
|
||||||
|
ITemplateService templates,
|
||||||
ILogger<SmtpEmailSender> log)
|
ILogger<SmtpEmailSender> log)
|
||||||
{
|
{
|
||||||
_smtp = smtp.Value;
|
_smtp = smtp.Value;
|
||||||
_contact = contact.Value;
|
_contact = contact.Value;
|
||||||
_subscribe = subscribe.Value;
|
_subscribe = subscribe.Value;
|
||||||
_fileStorage = fileStorage.Value;
|
_fileStorage = fileStorage.Value;
|
||||||
|
_templates = templates;
|
||||||
_log = log;
|
_log = log;
|
||||||
// Use APP_ENVIRONMENT_NAME from environment variable (set in docker-compose) with fallback to "Development"
|
|
||||||
_environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development";
|
_environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendContactAsync(ContactRequest req, CancellationToken ct)
|
public async Task SendContactAsync(ContactRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Throw error if ToEmail is not configured, since contact requests are important to process.
|
|
||||||
if (string.IsNullOrWhiteSpace(_contact.ToEmail))
|
if (string.IsNullOrWhiteSpace(_contact.ToEmail))
|
||||||
{
|
{
|
||||||
_log.LogDebug("Contact email skipped - ToEmail not configured");
|
_log.LogDebug("Contact email skipped - ToEmail not configured");
|
||||||
@@ -71,7 +74,6 @@ namespace Api.Services
|
|||||||
|
|
||||||
public async Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct)
|
public async Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Throw error if ToEmail is not configured, since subscription requests are important to process.
|
|
||||||
if (string.IsNullOrWhiteSpace(_subscribe.ToEmail))
|
if (string.IsNullOrWhiteSpace(_subscribe.ToEmail))
|
||||||
{
|
{
|
||||||
_log.LogDebug("Subscription email skipped - ToEmail not configured");
|
_log.LogDebug("Subscription email skipped - ToEmail not configured");
|
||||||
@@ -101,7 +103,6 @@ namespace Api.Services
|
|||||||
|
|
||||||
public async Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct)
|
public async Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Skip sending if ToEmail is not configured
|
|
||||||
if (string.IsNullOrWhiteSpace(_fileStorage.ToEmail))
|
if (string.IsNullOrWhiteSpace(_fileStorage.ToEmail))
|
||||||
{
|
{
|
||||||
_log.LogDebug("File download notification skipped - ToEmail not configured");
|
_log.LogDebug("File download notification skipped - ToEmail not configured");
|
||||||
@@ -135,8 +136,6 @@ namespace Api.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ConnectAndAuthenticateAsync(SmtpClient client, CancellationToken ct)
|
private async Task ConnectAndAuthenticateAsync(SmtpClient client, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// If you're in enterprise environments, you may need to tweak certificate validation.
|
|
||||||
// Don't disable it casually.
|
|
||||||
var tls = _smtp.UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
|
var tls = _smtp.UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
|
||||||
|
|
||||||
_log.LogDebug("Connecting to SMTP server {Host}:{Port} with security={Security}",
|
_log.LogDebug("Connecting to SMTP server {Host}:{Port} with security={Security}",
|
||||||
@@ -214,41 +213,41 @@ namespace Api.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string? jobSearchLink = null)
|
public string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7)
|
||||||
{
|
{
|
||||||
var body = $@"CV Matcher result
|
var strengths = result.Strengths?.Count > 0
|
||||||
|
? "- " + string.Join("\n- ", result.Strengths)
|
||||||
|
: string.Empty;
|
||||||
|
var gaps = result.Gaps?.Count > 0
|
||||||
|
? "- " + string.Join("\n- ", result.Gaps)
|
||||||
|
: string.Empty;
|
||||||
|
var recommendations = result.Recommendations?.Count > 0
|
||||||
|
? "- " + string.Join("\n- ", result.Recommendations)
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
CV Document ID: {cvDocumentId}
|
var body = _templates.Render("email.match.body", language,
|
||||||
Job: {jobLabel ?? "N/A"}
|
("cvDocumentId", cvDocumentId),
|
||||||
Job URL: {result.JobUrl ?? "N/A"}
|
("jobLabel", jobLabel ?? "N/A"),
|
||||||
Score: {result.Score}%
|
("jobUrl", result.JobUrl ?? "N/A"),
|
||||||
|
("score", result.Score.ToString()),
|
||||||
Summary:
|
("summary", result.Summary ?? string.Empty),
|
||||||
{result.Summary}
|
("strengths", strengths),
|
||||||
|
("gaps", gaps),
|
||||||
Strengths:
|
("recommendations", recommendations));
|
||||||
- {string.Join("\n- ", result.Strengths)}
|
|
||||||
|
|
||||||
Gaps:
|
|
||||||
- {string.Join("\n- ", result.Gaps)}
|
|
||||||
|
|
||||||
Recommendations:
|
|
||||||
- {string.Join("\n- ", result.Recommendations)}";
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(jobSearchLink))
|
if (!string.IsNullOrWhiteSpace(jobSearchLink))
|
||||||
{
|
{
|
||||||
body += $@"
|
body += _templates.Render("email.match.job-search-footer", language,
|
||||||
|
("jobSearchLink", jobSearchLink),
|
||||||
---
|
("expiryDays", expiryDays.ToString()));
|
||||||
Vrei sa gasesti mai multe joburi potrivite CV-ului tau?
|
|
||||||
Click: {jobSearchLink}
|
|
||||||
(link valabil 7 zile)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string BuildMatchEmailSubject(int score, string? jobLabel)
|
public string BuildMatchEmailSubject(int score, string? jobLabel, string language) =>
|
||||||
=> $"MyAi.ro CV Match: {score}% - {jobLabel ?? "Job"}";
|
_templates.Render("email.match.subject", language,
|
||||||
|
("score", score.ToString()),
|
||||||
|
("jobLabel", jobLabel ?? "Job"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
|
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
|
||||||
<ProjectReference Include="..\shared-models\shared-models.csproj" />
|
<ProjectReference Include="..\shared-models\shared-models.csproj" />
|
||||||
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
||||||
|
<ProjectReference Include="..\myai-models\myai-models.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ public sealed class CreateJobSearchTokenRequest
|
|||||||
{
|
{
|
||||||
public string CvDocumentId { get; set; } = string.Empty;
|
public string CvDocumentId { get; set; } = string.Empty;
|
||||||
public string Email { get; set; } = string.Empty;
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Language { get; set; } = "en";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public sealed class JobSearchController : ControllerBase
|
|||||||
if (string.IsNullOrWhiteSpace(request.CvDocumentId) || string.IsNullOrWhiteSpace(request.Email))
|
if (string.IsNullOrWhiteSpace(request.CvDocumentId) || string.IsNullOrWhiteSpace(request.Email))
|
||||||
return BadRequest(new ErrorResponse { Error = "CvDocumentId and Email are required.", Code = "invalid_request" });
|
return BadRequest(new ErrorResponse { Error = "CvDocumentId and Email are required.", Code = "invalid_request" });
|
||||||
|
|
||||||
var tokenId = await _tokenService.CreateTokenAsync(request.CvDocumentId, request.Email, ct);
|
var tokenId = await _tokenService.CreateTokenAsync(request.CvDocumentId, request.Email, request.Language, ct);
|
||||||
return Ok(new CreateJobSearchTokenResponse { TokenId = tokenId });
|
return Ok(new CreateJobSearchTokenResponse { TokenId = tokenId });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ COPY Apis/cv-matcher-api/cv-matcher-api.csproj Apis/cv-matcher-api/
|
|||||||
COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/
|
COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/
|
||||||
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
|
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
|
||||||
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
|
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
|
||||||
|
COPY Apis/myai-models/myai-models.csproj Apis/myai-models/
|
||||||
COPY Helpers/common-helpers/common-helpers.csproj Helpers/common-helpers/
|
COPY Helpers/common-helpers/common-helpers.csproj Helpers/common-helpers/
|
||||||
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
|
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ COPY Apis/cv-matcher-api/ Apis/cv-matcher-api/
|
|||||||
COPY Apis/cv-search-models/ Apis/cv-search-models/
|
COPY Apis/cv-search-models/ Apis/cv-search-models/
|
||||||
COPY Apis/shared-models/ Apis/shared-models/
|
COPY Apis/shared-models/ Apis/shared-models/
|
||||||
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
|
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
|
||||||
|
COPY Apis/myai-models/ Apis/myai-models/
|
||||||
COPY Helpers/common-helpers/ Helpers/common-helpers/
|
COPY Helpers/common-helpers/ Helpers/common-helpers/
|
||||||
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
|
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ using CvMatcher.Models.Settings;
|
|||||||
using CvSearch.Models.Data;
|
using CvSearch.Models.Data;
|
||||||
using CvSearch.Models.Settings;
|
using CvSearch.Models.Settings;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MyAi.Models.Data;
|
||||||
|
using MyAi.Models.Services;
|
||||||
using Refit;
|
using Refit;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Shared.Models.Settings;
|
using Shared.Models.Settings;
|
||||||
@@ -74,6 +76,17 @@ try
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<MyAiDbContext>(options =>
|
||||||
|
{
|
||||||
|
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
|
||||||
|
options.UseSqlServer(connectionString, sql =>
|
||||||
|
{
|
||||||
|
sql.MigrationsAssembly("myai-models");
|
||||||
|
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<ITemplateService, DbTemplateService>();
|
||||||
|
|
||||||
builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>();
|
builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>();
|
||||||
builder.Services.AddScoped<ICvMatcherService, CvMatcherService>();
|
builder.Services.AddScoped<ICvMatcherService, CvMatcherService>();
|
||||||
builder.Services.AddScoped<IJobTokenService, JobTokenService>();
|
builder.Services.AddScoped<IJobTokenService, JobTokenService>();
|
||||||
@@ -109,6 +122,11 @@ try
|
|||||||
var db = scope.ServiceProvider.GetRequiredService<CvSearchDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<CvSearchDbContext>();
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
}
|
}
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
Log.Information("{Service} startup complete", ServiceName);
|
Log.Information("{Service} startup complete", ServiceName);
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ namespace Api.Services.Contracts;
|
|||||||
|
|
||||||
public interface IJobTokenService
|
public interface IJobTokenService
|
||||||
{
|
{
|
||||||
Task<string> CreateTokenAsync(string cvDocumentId, string email, CancellationToken ct);
|
Task<string> CreateTokenAsync(string cvDocumentId, string email, string language, CancellationToken ct);
|
||||||
Task<string> TriggerStartAsync(string tokenId, CancellationToken ct);
|
Task<string> TriggerStartAsync(string tokenId, CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using CvMatcher.Models.Responses;
|
|||||||
using CvMatcher.Models.Settings;
|
using CvMatcher.Models.Settings;
|
||||||
using Api.Services.Contracts;
|
using Api.Services.Contracts;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using MyAi.Models.Services;
|
||||||
|
|
||||||
namespace Api.Services;
|
namespace Api.Services;
|
||||||
|
|
||||||
@@ -17,19 +18,22 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
private readonly IMatcherAiClient _ai;
|
private readonly IMatcherAiClient _ai;
|
||||||
private readonly IMatcherRepository _repository;
|
private readonly IMatcherRepository _repository;
|
||||||
private readonly MatcherSettings _settings;
|
private readonly MatcherSettings _settings;
|
||||||
|
private readonly ITemplateService _templates;
|
||||||
|
|
||||||
public CvMatcherService(
|
public CvMatcherService(
|
||||||
IRagApiClient rag,
|
IRagApiClient rag,
|
||||||
IJobTextExtractor jobTextExtractor,
|
IJobTextExtractor jobTextExtractor,
|
||||||
IMatcherAiClient ai,
|
IMatcherAiClient ai,
|
||||||
IMatcherRepository repository,
|
IMatcherRepository repository,
|
||||||
IOptions<MatcherSettings> options)
|
IOptions<MatcherSettings> options,
|
||||||
|
ITemplateService templates)
|
||||||
{
|
{
|
||||||
_rag = rag;
|
_rag = rag;
|
||||||
_jobTextExtractor = jobTextExtractor;
|
_jobTextExtractor = jobTextExtractor;
|
||||||
_ai = ai;
|
_ai = ai;
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_settings = options.Value;
|
_settings = options.Value;
|
||||||
|
_templates = templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct)
|
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct)
|
||||||
@@ -111,12 +115,8 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
var evidence = evidenceChunks.Count > 0 ? string.Join("\n\n", evidenceChunks.Take(4)) : Limit(job.Text, 4000);
|
var evidence = evidenceChunks.Count > 0 ? string.Join("\n\n", evidenceChunks.Take(4)) : Limit(job.Text, 4000);
|
||||||
var languageName = LanguageName(language);
|
var languageName = LanguageName(language);
|
||||||
|
|
||||||
var systemPrompt = $$"""
|
var systemPrompt = _templates.Render("ai.cv-match.system-prompt", "*",
|
||||||
You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100.
|
("languageName", languageName));
|
||||||
Penalize missing required skills. Do not invent experience. Use concise business language.
|
|
||||||
Respond entirely in {{languageName}} — all text fields in the JSON must be in {{languageName}}.
|
|
||||||
JSON shape: {"score":number,"summary":"...","strengths":["..."],"gaps":["..."],"recommendations":["..."],"evidence":["..."]}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var userPrompt = $"""
|
var userPrompt = $"""
|
||||||
CV:
|
CV:
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ public sealed class JobTokenService : IJobTokenService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> CreateTokenAsync(string cvDocumentId, string email, CancellationToken ct)
|
public async Task<string> CreateTokenAsync(string cvDocumentId, string email, string language, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var token = new JobSearchTokenEntity
|
var token = new JobSearchTokenEntity
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
CvDocumentId = cvDocumentId,
|
CvDocumentId = cvDocumentId,
|
||||||
Email = email,
|
Email = email,
|
||||||
|
Language = language,
|
||||||
ExpiresAt = DateTime.UtcNow.AddDays(_settings.TokenExpiryDays),
|
ExpiresAt = DateTime.UtcNow.AddDays(_settings.TokenExpiryDays),
|
||||||
Used = false,
|
Used = false,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
@@ -71,6 +72,7 @@ public sealed class JobTokenService : IJobTokenService
|
|||||||
TokenId = token.Id,
|
TokenId = token.Id,
|
||||||
CvDocumentId = token.CvDocumentId,
|
CvDocumentId = token.CvDocumentId,
|
||||||
Email = token.Email,
|
Email = token.Email,
|
||||||
|
Language = token.Language,
|
||||||
Status = JobSearchStatus.Pending,
|
Status = JobSearchStatus.Pending,
|
||||||
Keywords = keywords,
|
Keywords = keywords,
|
||||||
ProviderConfigJson = providerConfigJson,
|
ProviderConfigJson = providerConfigJson,
|
||||||
|
|||||||
@@ -82,5 +82,6 @@
|
|||||||
<ProjectReference Include="..\cv-search-models\cv-search-models.csproj" />
|
<ProjectReference Include="..\cv-search-models\cv-search-models.csproj" />
|
||||||
<ProjectReference Include="..\shared-models\shared-models.csproj" />
|
<ProjectReference Include="..\shared-models\shared-models.csproj" />
|
||||||
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
||||||
|
<ProjectReference Include="..\myai-models\myai-models.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public sealed class CvSearchDbContext : DbContext
|
|||||||
entity.Property(x => x.Id).HasMaxLength(64);
|
entity.Property(x => x.Id).HasMaxLength(64);
|
||||||
entity.Property(x => x.CvDocumentId).HasMaxLength(64).IsRequired();
|
entity.Property(x => x.CvDocumentId).HasMaxLength(64).IsRequired();
|
||||||
entity.Property(x => x.Email).HasMaxLength(256).IsRequired();
|
entity.Property(x => x.Email).HasMaxLength(256).IsRequired();
|
||||||
|
entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired();
|
||||||
entity.Property(x => x.Used).HasDefaultValue(false);
|
entity.Property(x => x.Used).HasDefaultValue(false);
|
||||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
});
|
});
|
||||||
@@ -40,6 +41,7 @@ public sealed class CvSearchDbContext : DbContext
|
|||||||
entity.Property(x => x.Status).HasMaxLength(32).IsRequired();
|
entity.Property(x => x.Status).HasMaxLength(32).IsRequired();
|
||||||
entity.Property(x => x.Keywords).HasMaxLength(1000);
|
entity.Property(x => x.Keywords).HasMaxLength(1000);
|
||||||
entity.Property(x => x.ProviderConfigJson).IsRequired(false);
|
entity.Property(x => x.ProviderConfigJson).IsRequired(false);
|
||||||
|
entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired();
|
||||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
entity.HasIndex(x => x.Status);
|
entity.HasIndex(x => x.Status);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public sealed class JobSearchSessionEntity
|
|||||||
public string Status { get; set; } = JobSearchStatus.Pending;
|
public string Status { get; set; } = JobSearchStatus.Pending;
|
||||||
public string Keywords { get; set; } = string.Empty;
|
public string Keywords { get; set; } = string.Empty;
|
||||||
public string? ProviderConfigJson { get; set; }
|
public string? ProviderConfigJson { get; set; }
|
||||||
|
public string Language { get; set; } = "en";
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public sealed class JobSearchTokenEntity
|
|||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
public string CvDocumentId { get; set; } = string.Empty;
|
public string CvDocumentId { get; set; } = string.Empty;
|
||||||
public string Email { get; set; } = string.Empty;
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Language { get; set; } = "en";
|
||||||
public DateTime ExpiresAt { get; set; }
|
public DateTime ExpiresAt { get; set; }
|
||||||
public bool Used { get; set; }
|
public bool Used { get; set; }
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
Generated
+174
@@ -0,0 +1,174 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CvSearch.Models.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CvSearch.Models.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CvSearchDbContext))]
|
||||||
|
[Migration("20260524145702_AddLanguageToJobSearchEntities")]
|
||||||
|
partial class AddLanguageToJobSearchEntities
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("cvSearch")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvSearch.Models.Data.Entities.JobSearchResultEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("JobText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("JobTitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("nvarchar(512)");
|
||||||
|
|
||||||
|
b.Property<string>("JobUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("nvarchar(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("ResultJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("Score")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.ToTable("JobSearchResults", "cvSearch");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvSearch.Models.Data.Entities.JobSearchSessionEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("CvDocumentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Keywords")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)")
|
||||||
|
.HasDefaultValue("en");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderConfigJson")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("nvarchar(32)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("JobSearchSessions", "cvSearch");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvSearch.Models.Data.Entities.JobSearchTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("CvDocumentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)")
|
||||||
|
.HasDefaultValue("en");
|
||||||
|
|
||||||
|
b.Property<bool>("Used")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("JobSearchTokens", "cvSearch");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CvSearch.Models.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddLanguageToJobSearchEntities : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Language",
|
||||||
|
schema: "cvSearch",
|
||||||
|
table: "JobSearchTokens",
|
||||||
|
type: "nvarchar(8)",
|
||||||
|
maxLength: 8,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "en");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Language",
|
||||||
|
schema: "cvSearch",
|
||||||
|
table: "JobSearchSessions",
|
||||||
|
type: "nvarchar(8)",
|
||||||
|
maxLength: 8,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Language",
|
||||||
|
schema: "cvSearch",
|
||||||
|
table: "JobSearchTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Language",
|
||||||
|
schema: "cvSearch",
|
||||||
|
table: "JobSearchSessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,13 @@ namespace CvSearch.Models.Migrations
|
|||||||
.HasMaxLength(1000)
|
.HasMaxLength(1000)
|
||||||
.HasColumnType("nvarchar(1000)");
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)")
|
||||||
|
.HasDefaultValue("en");
|
||||||
|
|
||||||
b.Property<string>("ProviderConfigJson")
|
b.Property<string>("ProviderConfigJson")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
@@ -142,6 +149,13 @@ namespace CvSearch.Models.Migrations
|
|||||||
b.Property<DateTime>("ExpiresAt")
|
b.Property<DateTime>("ExpiresAt")
|
||||||
.HasColumnType("datetime2");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)")
|
||||||
|
.HasDefaultValue("en");
|
||||||
|
|
||||||
b.Property<bool>("Used")
|
b.Property<bool>("Used")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("bit")
|
.HasColumnType("bit")
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MyAi.Models.Data.Entities;
|
||||||
|
|
||||||
|
public sealed class TemplateEntity
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
public string Language { get; set; } = string.Empty;
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using MyAi.Models.Data.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace MyAi.Models.Data;
|
||||||
|
|
||||||
|
public sealed class MyAiDbContext : DbContext
|
||||||
|
{
|
||||||
|
public const string SchemaName = "myAi";
|
||||||
|
public const string MigrationTableName = "_MyAiMigrations";
|
||||||
|
|
||||||
|
public MyAiDbContext(DbContextOptions<MyAiDbContext> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<TemplateEntity> Templates => Set<TemplateEntity>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.HasDefaultSchema(SchemaName);
|
||||||
|
|
||||||
|
modelBuilder.Entity<TemplateEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Templates");
|
||||||
|
entity.HasKey(x => new { x.Key, x.Language });
|
||||||
|
entity.Property(x => x.Key).HasMaxLength(128);
|
||||||
|
entity.Property(x => x.Language).HasMaxLength(8);
|
||||||
|
entity.Property(x => x.Value).IsRequired();
|
||||||
|
entity.Property(x => x.Description).HasMaxLength(500).HasDefaultValue(string.Empty);
|
||||||
|
entity.Property(x => x.UpdatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using MyAi.Models.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MyAi.Models.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MyAiDbContext))]
|
||||||
|
[Migration("20260524145351_AddTemplates")]
|
||||||
|
partial class AddTemplates
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("myAi")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyAi.Models.Data.Entities.TemplateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Language");
|
||||||
|
|
||||||
|
b.ToTable("Templates", "myAi");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MyAi.Models.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTemplates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.EnsureSchema(
|
||||||
|
name: "myAi");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Templates",
|
||||||
|
schema: "myAi",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||||
|
Language = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false, defaultValue: ""),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Templates", x => new { x.Key, x.Language });
|
||||||
|
});
|
||||||
|
|
||||||
|
Seed(migrationBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Seed(MigrationBuilder m)
|
||||||
|
{
|
||||||
|
void Row(string key, string lang, string value, string description = "")
|
||||||
|
=> m.InsertData("Templates", ["Key", "Language", "Value", "Description"], [key, lang, value, description], "myAi");
|
||||||
|
|
||||||
|
// Match result email — subject
|
||||||
|
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
|
||||||
|
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");
|
||||||
|
Row("email.match.body", "ro",
|
||||||
|
"Rezultat potrivire CV\n\nID document CV: {{cvDocumentId}}\nJob: {{jobLabel}}\nURL job: {{jobUrl}}\nScor: {{score}}%\n\nRezumat:\n{{summary}}\n\nPuncte forte:\n{{strengths}}\n\nLipsuri:\n{{gaps}}\n\nRecomandări:\n{{recommendations}}",
|
||||||
|
"Corpul emailului pentru rezultatul potrivirii CV");
|
||||||
|
|
||||||
|
// Match result email — job search CTA footer
|
||||||
|
Row("email.match.job-search-footer", "en",
|
||||||
|
"\n\n---\nWant to find more jobs matching your CV?\nClick: {{jobSearchLink}}\n(link valid for {{expiryDays}} days)",
|
||||||
|
"Job search CTA appended to match result email");
|
||||||
|
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");
|
||||||
|
|
||||||
|
// 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 — 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");
|
||||||
|
|
||||||
|
// HTML job-search start page messages
|
||||||
|
Row("html.job-search.started.title", "en", "Job search started", "Title for job search started page");
|
||||||
|
Row("html.job-search.started.message", "en", "Your job search has started. Results will be sent to your email shortly.", "Message for job search started page");
|
||||||
|
Row("html.job-search.started.title", "ro", "Căutare joburi pornită", "Titlu pagina cautare joburi pornita");
|
||||||
|
Row("html.job-search.started.message", "ro", "Căutarea joburilor a început. Rezultatele vor fi trimise pe email în scurt timp.", "Mesaj pagina cautare joburi pornita");
|
||||||
|
|
||||||
|
Row("html.job-search.already-used.title", "en", "Link already used", "Title for already-used page");
|
||||||
|
Row("html.job-search.already-used.message", "en", "This job search link has already been used.", "Message for already-used page");
|
||||||
|
Row("html.job-search.already-used.title", "ro", "Link deja folosit", "Titlu pagina link deja folosit");
|
||||||
|
Row("html.job-search.already-used.message", "ro", "Acest link de cautare joburi a fost deja folosit.", "Mesaj pagina link deja folosit");
|
||||||
|
|
||||||
|
Row("html.job-search.expired.title", "en", "Link expired", "Title for expired link page");
|
||||||
|
Row("html.job-search.expired.message", "en", "This job search link has expired. Please request a new CV match to get a fresh link.", "Message for expired link page");
|
||||||
|
Row("html.job-search.expired.title", "ro", "Link expirat", "Titlu pagina link expirat");
|
||||||
|
Row("html.job-search.expired.message", "ro", "Acest link de cautare joburi a expirat. Solicita o noua potrivire CV pentru a primi un link nou.", "Mesaj pagina link expirat");
|
||||||
|
|
||||||
|
Row("html.job-search.invalid.title", "en", "Invalid link", "Title for invalid link page");
|
||||||
|
Row("html.job-search.invalid.message", "en", "This job search link is not valid.", "Message for invalid link page");
|
||||||
|
Row("html.job-search.invalid.title", "ro", "Link invalid", "Titlu pagina link invalid");
|
||||||
|
Row("html.job-search.invalid.message", "ro", "Acest link de cautare joburi nu este valid.", "Mesaj pagina link invalid");
|
||||||
|
|
||||||
|
Row("html.job-search.error.title", "en", "Error", "Title for error page");
|
||||||
|
Row("html.job-search.error.message", "en", "An error occurred. Please try again later.", "Message for error page");
|
||||||
|
Row("html.job-search.error.title", "ro", "Eroare", "Titlu pagina eroare");
|
||||||
|
Row("html.job-search.error.message", "ro", "A apărut o eroare. Te rugăm să încerci din nou mai târziu.", "Mesaj pagina eroare");
|
||||||
|
|
||||||
|
// AI system prompt for CV matching (language is a {{languageName}} variable inside it)
|
||||||
|
Row("ai.cv-match.system-prompt", "*",
|
||||||
|
"You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100.\nPenalize missing required skills. Do not invent experience. Use concise business language.\nRespond entirely in {{languageName}} — all text fields in the JSON must be in {{languageName}}.\nJSON shape: {\"score\":number,\"summary\":\"...\",\"strengths\":[\"...\"],\"gaps\":[\"...\"],\"recommendations\":[\"...\"],\"evidence\":[\"...\"]}",
|
||||||
|
"System prompt template for the CV-to-job LLM matching call. {{languageName}} is substituted at runtime.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Templates",
|
||||||
|
schema: "myAi");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using MyAi.Models.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MyAi.Models.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MyAiDbContext))]
|
||||||
|
partial class MyAiDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("myAi")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MyAi.Models.Data.Entities.TemplateEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Language");
|
||||||
|
|
||||||
|
b.ToTable("Templates", "myAi");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MyAi.Models.Data;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MyAi.Models.Services;
|
||||||
|
|
||||||
|
public sealed class DbTemplateService : ITemplateService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<DbTemplateService> _logger;
|
||||||
|
private ConcurrentDictionary<string, string> _cache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private DateTime _loadedAt = DateTime.MinValue;
|
||||||
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
|
||||||
|
|
||||||
|
public DbTemplateService(IServiceScopeFactory scopeFactory, ILogger<DbTemplateService> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Get(string key, string language = "en")
|
||||||
|
{
|
||||||
|
EnsureCacheLoaded();
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(CacheKey(key, language), out var value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& _cache.TryGetValue(CacheKey(key, "en"), out var fallback))
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
_logger.LogWarning("Template not found: key={Key}, language={Language}", key, language);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Render(string key, string language, params (string Key, string Value)[] placeholders)
|
||||||
|
{
|
||||||
|
var template = Get(key, language);
|
||||||
|
foreach (var (k, v) in placeholders)
|
||||||
|
template = template.Replace($"{{{{{k}}}}}", v, StringComparison.OrdinalIgnoreCase);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureCacheLoaded()
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow - _loadedAt < CacheTtl) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
|
||||||
|
var rows = db.Templates.AsNoTracking().ToList();
|
||||||
|
var fresh = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var row in rows)
|
||||||
|
fresh[CacheKey(row.Key, row.Language)] = row.Value;
|
||||||
|
|
||||||
|
_cache = fresh;
|
||||||
|
_loadedAt = DateTime.UtcNow;
|
||||||
|
_logger.LogDebug("Template cache refreshed. {Count} templates loaded.", rows.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to refresh template cache. Serving stale cache.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CacheKey(string key, string language) => $"{key}::{language}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MyAi.Models.Services;
|
||||||
|
|
||||||
|
public interface ITemplateService
|
||||||
|
{
|
||||||
|
string Get(string key, string language = "en");
|
||||||
|
string Render(string key, string language, params (string Key, string Value)[] placeholders);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RootNamespace>MyAi.Models</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -7,6 +7,7 @@ COPY Jobs/job-scheduler/job-scheduler.csproj Jobs/job-scheduler/
|
|||||||
COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/
|
COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/
|
||||||
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
|
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
|
||||||
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
|
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
|
||||||
|
COPY Apis/myai-models/myai-models.csproj Apis/myai-models/
|
||||||
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
|
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
|
||||||
|
|
||||||
RUN dotnet restore Jobs/cv-search-job/cv-search-job.csproj
|
RUN dotnet restore Jobs/cv-search-job/cv-search-job.csproj
|
||||||
@@ -16,6 +17,7 @@ COPY Jobs/job-scheduler/ Jobs/job-scheduler/
|
|||||||
COPY Apis/cv-search-models/ Apis/cv-search-models/
|
COPY Apis/cv-search-models/ Apis/cv-search-models/
|
||||||
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
|
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
|
||||||
COPY Apis/shared-models/ Apis/shared-models/
|
COPY Apis/shared-models/ Apis/shared-models/
|
||||||
|
COPY Apis/myai-models/ Apis/myai-models/
|
||||||
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
|
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
|
||||||
|
|
||||||
RUN dotnet publish Jobs/cv-search-job/cv-search-job.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
RUN dotnet publish Jobs/cv-search-job/cv-search-job.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using JobScheduler.Tasks;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using MyAi.Models.Data;
|
||||||
|
using MyAi.Models.Services;
|
||||||
using Refit;
|
using Refit;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Shared.Models.Settings;
|
using Shared.Models.Settings;
|
||||||
@@ -51,6 +53,17 @@ try
|
|||||||
client.DefaultRequestHeaders.Add("X-Internal-Api-Key", key);
|
client.DefaultRequestHeaders.Add("X-Internal-Api-Key", key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<MyAiDbContext>(options =>
|
||||||
|
{
|
||||||
|
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
|
||||||
|
options.UseSqlServer(connectionString, sql =>
|
||||||
|
{
|
||||||
|
sql.MigrationsAssembly("myai-models");
|
||||||
|
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
builder.Services.AddSingleton<ITemplateService, DbTemplateService>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient<HtmlJobSearcher>();
|
builder.Services.AddHttpClient<HtmlJobSearcher>();
|
||||||
builder.Services.AddSingleton<CvSearchEmailSender>();
|
builder.Services.AddSingleton<CvSearchEmailSender>();
|
||||||
|
|
||||||
@@ -66,12 +79,17 @@ try
|
|||||||
|
|
||||||
host.LogHostStartupDiagnostics(ServiceName);
|
host.LogHostStartupDiagnostics(ServiceName);
|
||||||
|
|
||||||
Log.Information("Running EF Core migrations for CvSearchDbContext");
|
Log.Information("Running EF Core migrations");
|
||||||
using (var scope = host.Services.CreateScope())
|
using (var scope = host.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<CvSearchDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<CvSearchDbContext>();
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
}
|
}
|
||||||
|
using (var scope = host.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
Log.Information("{Service} startup complete. Background scheduler is running.", ServiceName);
|
Log.Information("{Service} startup complete. Background scheduler is running.", ServiceName);
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ using MailKit.Security;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
using MyAi.Models.Services;
|
||||||
|
|
||||||
namespace CvSearchJob.Services;
|
namespace CvSearchJob.Services;
|
||||||
|
|
||||||
public sealed class CvSearchEmailSender
|
public sealed class CvSearchEmailSender
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ITemplateService _templates;
|
||||||
private readonly ILogger<CvSearchEmailSender> _logger;
|
private readonly ILogger<CvSearchEmailSender> _logger;
|
||||||
|
|
||||||
public CvSearchEmailSender(IConfiguration config, ILogger<CvSearchEmailSender> logger)
|
public CvSearchEmailSender(IConfiguration config, ITemplateService templates, ILogger<CvSearchEmailSender> logger)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_templates = templates;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ public sealed class CvSearchEmailSender
|
|||||||
string toEmail,
|
string toEmail,
|
||||||
string? attachmentPath,
|
string? attachmentPath,
|
||||||
IReadOnlyList<JobSearchResultEntity> results,
|
IReadOnlyList<JobSearchResultEntity> results,
|
||||||
|
string language,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var smtpHost = _config["Smtp:Host"];
|
var smtpHost = _config["Smtp:Host"];
|
||||||
@@ -42,8 +46,9 @@ public sealed class CvSearchEmailSender
|
|||||||
|
|
||||||
if (recipients.Count == 0) return;
|
if (recipients.Count == 0) return;
|
||||||
|
|
||||||
var body = BuildBody(results);
|
var body = BuildBody(results, language);
|
||||||
var subject = $"MyAi.ro: {results.Count} joburi potrivite CV-ului tau";
|
var subject = _templates.Render("email.search-results.subject", language,
|
||||||
|
("count", results.Count.ToString()));
|
||||||
var environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development";
|
var environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development";
|
||||||
|
|
||||||
foreach (var recipient in recipients)
|
foreach (var recipient in recipients)
|
||||||
@@ -77,27 +82,26 @@ public sealed class CvSearchEmailSender
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildBody(IReadOnlyList<JobSearchResultEntity> results)
|
private string BuildBody(IReadOnlyList<JobSearchResultEntity> results, string language)
|
||||||
{
|
{
|
||||||
if (results.Count == 0)
|
if (results.Count == 0)
|
||||||
return "MyAi.ro nu a gasit joburi care sa corespunda CV-ului tau. Incercati mai tarziu sau ajustati CV-ul.";
|
return _templates.Get("email.search-results.empty", language);
|
||||||
|
|
||||||
var lines = new System.Text.StringBuilder();
|
|
||||||
lines.AppendLine($"MyAi.ro a gasit {results.Count} joburi potrivite CV-ului tau:");
|
|
||||||
lines.AppendLine();
|
|
||||||
|
|
||||||
|
var items = new System.Text.StringBuilder();
|
||||||
for (int i = 0; i < results.Count; i++)
|
for (int i = 0; i < results.Count; i++)
|
||||||
{
|
{
|
||||||
var r = results[i];
|
var r = results[i];
|
||||||
var matchResp = TryParseResult(r.ResultJson);
|
var matchResp = TryParseResult(r.ResultJson);
|
||||||
lines.AppendLine($"{i + 1}. {r.JobTitle} ({r.Score}% match) [{r.ProviderName}]");
|
items.AppendLine($"{i + 1}. {r.JobTitle} ({r.Score}% match) [{r.ProviderName}]");
|
||||||
lines.AppendLine($" {r.JobUrl}");
|
items.AppendLine($" {r.JobUrl}");
|
||||||
if (matchResp is not null && !string.IsNullOrWhiteSpace(matchResp.Summary))
|
if (matchResp is not null && !string.IsNullOrWhiteSpace(matchResp.Summary))
|
||||||
lines.AppendLine($" {matchResp.Summary}");
|
items.AppendLine($" {matchResp.Summary}");
|
||||||
lines.AppendLine();
|
items.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.ToString();
|
return _templates.Render("email.search-results.body", language,
|
||||||
|
("count", results.Count.ToString()),
|
||||||
|
("items", items.ToString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JobMatchResponse? TryParseResult(string json)
|
private static JobMatchResponse? TryParseResult(string json)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public sealed class CvSearchJobTask : IJobTask
|
|||||||
await db.SaveChangesAsync(cancellationToken);
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
var attachmentPath = BuildCvPath(pending.CvDocumentId);
|
var attachmentPath = BuildCvPath(pending.CvDocumentId);
|
||||||
await _emailSender.SendResultsAsync(pending.Email, attachmentPath, results, cancellationToken);
|
await _emailSender.SendResultsAsync(pending.Email, attachmentPath, results, pending.Language, cancellationToken);
|
||||||
_logger.LogInformation("Session {SessionId} done. {Count} results sent.", pending.Id, results.Count);
|
_logger.LogInformation("Session {SessionId} done. {Count} results sent.", pending.Id, results.Count);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<ProjectReference Include="..\..\Apis\shared-models\shared-models.csproj" />
|
<ProjectReference Include="..\..\Apis\shared-models\shared-models.csproj" />
|
||||||
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
||||||
<ProjectReference Include="..\job-scheduler\job-scheduler.csproj" />
|
<ProjectReference Include="..\job-scheduler\job-scheduler.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Apis\myai-models\myai-models.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.2.11415.280
|
VisualStudioVersion = 18.2.11415.280
|
||||||
@@ -38,72 +39,206 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-job", "Jobs\cv-se
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "job-scheduler", "Jobs\job-scheduler\job-scheduler.csproj", "{A19D2776-B935-BD35-4AB1-3FCE2092805A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "job-scheduler", "Jobs\job-scheduler\job-scheduler.csproj", "{A19D2776-B935-BD35-4AB1-3FCE2092805A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "myai-models", "Apis\myai-models\myai-models.csproj", "{3BE2E134-E773-4574-ABDD-175F00E4932E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B0A3EAB7-759A-448A-A906-52DF75A70016}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|x86.ActiveCfg = Release|x86
|
||||||
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7446D193-8636-4E58-96E4-0C8CB8790679}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -113,15 +248,16 @@ Global
|
|||||||
{A63E1C1A-4A78-49F4-9F5C-D43783294861} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
{A63E1C1A-4A78-49F4-9F5C-D43783294861} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
|
{B2C3D4E5-F6A7-4890-BCDE-F01234567890} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
{6A1ADA81-28E9-4A64-A32D-0755876D5EB7} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
{185A8BB0-344A-4856-AEB4-213866EB2EE7} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
{185A8BB0-344A-4856-AEB4-213866EB2EE7} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
{7446D193-8636-4E58-96E4-0C8CB8790679} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284}
|
{7446D193-8636-4E58-96E4-0C8CB8790679} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284}
|
||||||
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284}
|
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284}
|
||||||
{B2C3D4E5-F6A7-4890-BCDE-F01234567890} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
|
||||||
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
|
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
|
||||||
{C3D4E5F6-A7B8-4901-CDEF-012345678901} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
|
{C3D4E5F6-A7B8-4901-CDEF-012345678901} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
|
||||||
{A19D2776-B935-BD35-4AB1-3FCE2092805A} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
|
{A19D2776-B935-BD35-4AB1-3FCE2092805A} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
|
||||||
|
{3BE2E134-E773-4574-ABDD-175F00E4932E} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {6246A67B-299E-4E64-8DBE-1A66771E7C67}
|
SolutionGuid = {6246A67B-299E-4E64-8DBE-1A66771E7C67}
|
||||||
|
|||||||
Reference in New Issue
Block a user