diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index 29a5068c9f..f897b99dda 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -53,6 +53,9 @@ if ($.fn.dataTable !== undefined && $.fn.dataTable.ext) { return csv; }, }; + + $.fn.dataTable.Buttons.defaults.dom.button.className = 'btn custom-table-btn flex-none'; + $.fn.dataTable.Buttons.defaults.dom.button.liner.tag = false; } // ============================================================================ diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/AIProviderResult.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/AIProviderResult.cs index fc5105df82..a3b117062a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/AIProviderResult.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/AIProviderResult.cs @@ -5,6 +5,7 @@ internal sealed record AIProviderResult( string RawResponse = "", string? Model = null, string? FinishReason = null, + int? HttpStatusCode = null, int? PromptTokens = null, int? CompletionTokens = null, int? TotalTokens = null, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/OpenAIRuntimeService.cs index 9ca7329475..6335c3b765 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Runtime/OpenAIRuntimeService.cs @@ -143,7 +143,8 @@ public async Task GenerateApplicationAnalysisAsync( applicationAnalysisContent, systemPrompt, ApplicationAnalysisCompletionTokens, - operationName: ApplicationAnalysisPromptType), + operationName: ApplicationAnalysisPromptType, + promptVersion: promptVersion), AIProviderPayloadValidator.IsValidApplicationAnalysisJson, "application analysis"); await LogPromptOutputAsync(ApplicationAnalysisPromptType, promptVersion, result.CaptureOutput); @@ -161,7 +162,9 @@ private async Task GenerateSummaryAsync( string? systemPrompt, int maxTokens = 150, double? temperature = null, - string? operationName = null) + string? operationName = null, + string? promptVersion = null, + string? fileName = null) { var providerName = ResolveProviderName(operationName); if (!string.Equals(providerName, DefaultProviderName, StringComparison.Ordinal)) @@ -216,13 +219,17 @@ private async Task GenerateSummaryAsync( var response = await _httpClient.PostAsync(ResolveApiUrl(operationName), httpContent); var responseContent = await response.Content.ReadAsStringAsync(); var metadata = TryExtractProviderMetadata(responseContent); - var providerResponse = BuildProviderResponseFromMetadata(string.Empty, responseContent, metadata); + var providerResponse = BuildProviderResponseFromMetadata( + string.Empty, + responseContent, + metadata, + (int)response.StatusCode); _logger.LogDebug( "OpenAI chat completions response received. StatusCode: {StatusCode}, ResponseLength: {ResponseLength}", response.StatusCode, responseContent?.Length ?? 0); - LogProviderMetadata(operationName, providerResponse, response.IsSuccessStatusCode); + LogProviderMetadata(operationName, promptVersion, fileName, providerResponse, response.IsSuccessStatusCode); if (!response.IsSuccessStatusCode) { @@ -245,7 +252,11 @@ private async Task GenerateSummaryAsync( var modelOutput = message.GetProperty("content").GetString(); return string.IsNullOrWhiteSpace(modelOutput) ? AIOperationResult.InvalidOutput(providerResponse) - : AIOperationResult.Success(BuildProviderResponseFromMetadata(modelOutput, responseContent, metadata)); + : AIOperationResult.Success(BuildProviderResponseFromMetadata( + modelOutput, + responseContent, + metadata, + (int)response.StatusCode)); } return AIOperationResult.InvalidOutput(providerResponse); @@ -298,11 +309,13 @@ public async Task GenerateAttachmentSummaryAsync(Atta await LogPromptInputAsync(AttachmentSummaryPromptType, promptVersion, prompt, contentToAnalyze); var result = await GenerateWithRetryAsync( - () => GenerateSummaryAsync( - contentToAnalyze, - prompt, - AttachmentSummaryCompletionTokens, - operationName: AttachmentSummaryPromptType), + () => GenerateSummaryAsync( + contentToAnalyze, + prompt, + AttachmentSummaryCompletionTokens, + operationName: AttachmentSummaryPromptType, + promptVersion: promptVersion, + fileName: fileName), AIProviderPayloadValidator.IsValidAttachmentSummaryText, "attachment summary"); await LogPromptOutputAsync(AttachmentSummaryPromptType, promptVersion, result.CaptureOutput); @@ -462,11 +475,12 @@ public async Task GenerateApplicationScoringAsync(Ap await LogPromptInputAsync(ApplicationScoringPromptType, promptVersion, systemPrompt, applicationScoringContent); var result = await GenerateWithRetryAsync( - () => GenerateSummaryAsync( - applicationScoringContent, - systemPrompt, - ApplicationScoringCompletionTokens, - operationName: ApplicationScoringPromptType), + () => GenerateSummaryAsync( + applicationScoringContent, + systemPrompt, + ApplicationScoringCompletionTokens, + operationName: ApplicationScoringPromptType, + promptVersion: promptVersion), content => AIProviderPayloadValidator.IsValidApplicationScoringJson(content, sectionJson), $"application scoring section {request.SectionName}"); await LogPromptOutputAsync(ApplicationScoringPromptType, promptVersion, result.CaptureOutput); @@ -564,13 +578,18 @@ private static AIOperationResult MapFailureOutcome(HttpStatusCode statusCode, AI return AIOperationResult.PermanentFailure(response); } - private static AIProviderResult BuildProviderResponseFromMetadata(string content, string? rawResponse, AIProviderResponseMetadata? metadata) + private static AIProviderResult BuildProviderResponseFromMetadata( + string content, + string? rawResponse, + AIProviderResponseMetadata? metadata, + int? httpStatusCode = null) { return new AIProviderResult( content, rawResponse ?? string.Empty, metadata?.Model, metadata?.FinishReason, + httpStatusCode, metadata?.PromptTokens, metadata?.CompletionTokens, metadata?.TotalTokens, @@ -629,10 +648,16 @@ private static AIProviderResult BuildProviderResponseFromMetadata(string content } } - private void LogProviderMetadata(string? operationName, AIProviderResult response, bool success) + private void LogProviderMetadata( + string? operationName, + string? promptVersion, + string? fileName, + AIProviderResult response, + bool success) { if (string.IsNullOrWhiteSpace(response.Model) && string.IsNullOrWhiteSpace(response.FinishReason) + && response.HttpStatusCode == null && response.PromptTokens == null && response.CompletionTokens == null && response.TotalTokens == null @@ -644,21 +669,26 @@ private void LogProviderMetadata(string? operationName, AIProviderResult respons if (response.PromptTokens != null || response.CompletionTokens != null || response.TotalTokens != null) { _logger.LogInformation( - "AI token usage. FeatureName={FeatureName}, InputTokens={InputTokens}, CompletionTokens={CompletionTokens}, TotalTokens={TotalTokens}, Environment={Environment}, TenantId={TenantId}, Status={Status}", + "AI token usage. OperationName={OperationName}, InputTokens={InputTokens}, CompletionTokens={CompletionTokens}, TotalTokens={TotalTokens}, Environment={Environment}, TenantId={TenantId}, Status={Status}, PromptVersion={PromptVersion}, Model={Model}, HttpStatusCode={HttpStatusCode}, FileName={FileName}", operationName ?? "completion", response.PromptTokens, response.CompletionTokens, response.TotalTokens, _hostEnvironment.EnvironmentName, _currentTenant.Id, - success ? "success" : "failed"); + success ? "success" : "failed", + promptVersion, + response.Model, + response.HttpStatusCode, + fileName); } _logger.LogDebug( - "AI provider response metadata for {OperationName}: Model={Model}, FinishReason={FinishReason}, PromptTokens={PromptTokens}, CompletionTokens={CompletionTokens}, TotalTokens={TotalTokens}, ReasoningTokens={ReasoningTokens}", + "AI provider response metadata for {OperationName}: Model={Model}, FinishReason={FinishReason}, HttpStatusCode={HttpStatusCode}, PromptTokens={PromptTokens}, CompletionTokens={CompletionTokens}, TotalTokens={TotalTokens}, ReasoningTokens={ReasoningTokens}", operationName ?? "completion", response.Model, response.FinishReason, + response.HttpStatusCode, response.PromptTokens, response.CompletionTokens, response.TotalTokens, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs index dbf5272046..8c9585998f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Identity/UserImportAppService.cs @@ -166,7 +166,7 @@ public async Task> SearchAsync(UserSearchDto importUserSearchDto) } } - return users; + return users.GroupBy(u => u.UserGuid).Select(g => g.First()).ToList(); } private async Task CreateNewIdentityUserAsync(Guid newUserId, string? username, string? firstName, string? lastName, string? emailAddress) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml index e5b8c96942..960f806cc0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml @@ -241,6 +241,14 @@ + @* Zone Section : AdditionalContact *@ + + @await Component.InvokeAsync("ApplicationContactsWidget", new { applicationId = Model.ApplicationId }) + + @* Zone Section : Address *@ @@ -363,17 +371,6 @@ -@* Zone Section : AdditionalContact *@ - - - @await Component.InvokeAsync("ApplicationContactsWidget", new { applicationId = Model.ApplicationId }) - - - -