From 7bc90035b3015702bc7df8b0d86f9491ba9de20a Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Wed, 8 Jul 2026 16:41:54 -0700 Subject: [PATCH 01/22] feature/AB#32580-TemplateAttachments --- .../Events/ScheduledNotificationHelper.cs | 8 +++++--- .../Views/Shared/Components/EmailsWidget/Default.cshtml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs index db627a339..e03f81412 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Events/ScheduledNotificationHelper.cs @@ -53,9 +53,9 @@ public static Dictionary BuildTokenValues(Application applicatio ["submission_number"] = application.ReferenceNo, ["submission_date"] = application.SubmissionDate.ToString("yyyy-MM-dd"), ["status"] = applicationStatus?.StatusCode.ToString() ?? string.Empty, - ["approved_amount"] = application.ApprovedAmount.ToString("N2"), - ["requested_amount"] = application.RequestedAmount.ToString("N2"), - ["recommended_amount"] = application.RecommendedAmount.ToString("N2"), + ["approved_amount"] = application.ApprovedAmount.ToString("$#,##0.00"), + ["requested_amount"] = application.RequestedAmount.ToString("$#,##0.00"), + ["recommended_amount"] = application.RecommendedAmount.ToString("$#,##0.00"), ["approval_date"] = application.FinalDecisionDate?.ToString("yyyy-MM-dd") ?? string.Empty, ["decline_rationale"] = application.DeclineRational ?? string.Empty, ["community"] = application.Community ?? string.Empty, @@ -68,6 +68,8 @@ public static Dictionary BuildTokenValues(Application applicatio ["contact_full_name"] = applicantAgent?.Name ?? string.Empty, ["contact_title"] = applicantAgent?.Title ?? string.Empty, ["category"] = applicationForm?.Category ?? string.Empty, + ["today_date"] = $"Today, {DateTime.Today.ToString("MMMM d, yyyy")}", + ["unity_application_id"] = application.UnityApplicationId ?? string.Empty }; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml index af9364275..ce6c7ea1b 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml @@ -186,7 +186,7 @@ - + From a3f09b264c983bfc866285119c83ea51f0df8c73 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Thu, 9 Jul 2026 16:20:02 -0700 Subject: [PATCH 02/22] feature/AB#32580-TemplateAttachments-FixJsValidationError --- .../Views/Settings/NotificationsSettingGroup/Default.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js index 03527ab1b..4ae310285 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js @@ -305,7 +305,7 @@ $(function () { if (!sendFrom?.trim()) { validationErrors.push('Send From is required.'); markFieldError('sendFrom', 'Send From is required.'); - } else if (!isValidEmail(sendFrom.trim())) { + } else if (!validateEmail(sendFrom.trim())) { validationErrors.push('Send From must be a valid email address.'); markFieldError('sendFrom', 'Send From must be a valid email address.'); } From 591aee9cce406f3fddd7f3f497557ea0825349d5 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 7 Jul 2026 17:17:00 -0700 Subject: [PATCH 03/22] AB#33756 restore attachment status proxy method --- .../IGrantApplicationAppService.cs | 1 + .../GrantApplicationAppService.cs | 40 +++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs index b149d0d6c..351083a38 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IGrantApplicationAppService.cs @@ -23,6 +23,7 @@ public interface IGrantApplicationAppService Task GetAccountCodingIdFromFormIdAsync(Guid formId); Task DismissAIAnalysisItemAsync(Guid applicationId, string itemId); Task RestoreAIAnalysisItemAsync(Guid applicationId, string itemId); + Task GetAIGenerationStatusAsync(Guid applicationId, string operationType); Task> GetListAsync(GrantApplicationListInputDto input); Task IsApplicantRedStopAsync(Guid applicationId); string? GetWorkflowDiagram(bool isDirectApproval); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 935534184..2078de5b0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -57,11 +57,10 @@ public class GrantApplicationAppService( IApplicantAgentRepository applicantAgentRepository, IApplicantAddressRepository applicantAddressRepository, IApplicantSupplierAppService applicantSupplierService, - IPaymentRequestAppService paymentRequestService, - IApplicationAIGenerationQueue aiGenerationQueue, - IAIGenerationStatusAppService aiGenerationStatusAppService, - IAIRateLimiter aiRateLimiter, - IFeatureChecker featureChecker) + IPaymentRequestAppService paymentRequestService, + IAIGenerationStatusAppService aiGenerationStatusAppService, + IAIRateLimiter aiRateLimiter, + IFeatureChecker featureChecker) : GrantManagerAppService, IGrantApplicationAppService #pragma warning restore S107 // Methods should not have too many parameters { @@ -1358,14 +1357,29 @@ public async Task DismissAIAnalysisItemAsync(Guid applicationId, string return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: true); } - public async Task RestoreAIAnalysisItemAsync(Guid applicationId, string itemId) - { - return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: false); - } - - private async Task UpdateAIAnalysisItemDismissedStateAsync(Guid applicationId, string itemId, bool isDismissed) - { - if (string.IsNullOrWhiteSpace(itemId)) + public async Task RestoreAIAnalysisItemAsync(Guid applicationId, string itemId) + { + return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: false); + } + + public async Task GetAIGenerationStatusAsync(Guid applicationId, string operationType) + { + await EnsureAIGenerationStatusAccessAsync(operationType); + + var request = await aiGenerationStatusAppService.GetLatestAsync(applicationId, operationType, CurrentTenant.Id); + var state = await aiRateLimiter.GetStateAsync(); + + return new AIGenerationStatusDto + { + GenerationRequest = request, + IsGenerating = state.IsGenerating, + RetryAfterSeconds = state.RetryAfterSeconds + }; + } + + private async Task UpdateAIAnalysisItemDismissedStateAsync(Guid applicationId, string itemId, bool isDismissed) + { + if (string.IsNullOrWhiteSpace(itemId)) { throw new UserFriendlyException("AI analysis item id is required."); } From bbc6623f1e93be68819032c890ae031c98bb19f1 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 8 Jul 2026 11:55:53 -0700 Subject: [PATCH 04/22] AB#33756 align AI generation status contract --- .../Generation/AIGenerationRequestDto.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs new file mode 100644 index 000000000..febc00ed3 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs @@ -0,0 +1,12 @@ +using System; + +namespace Unity.AI.Generation; + +public class AIGenerationRequestDto +{ + public Guid ApplicationId { get; set; } + + public Guid OperationId { get; set; } + + public string OperationType { get; set; } = string.Empty; +} From cb30550d0f4f87d76e34d0057460dbd52fe974e9 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 8 Jul 2026 15:59:07 -0700 Subject: [PATCH 05/22] AB#33756 clean up AI route surface --- .../Generation/AIGenerationAppService.cs | 9 +++++++++ .../Pages/GrantApplications/ai-analysis.js | 15 +++------------ .../Components/AssessmentScoresWidget/Default.js | 10 ++-------- .../ChefsAttachments/ChefsAttachments.js | 5 +---- .../Shared/Components/ReviewList/ReviewList.js | 15 +++------------ .../Automation/AIGenerationAppServiceTests.cs | 11 +++++------ 6 files changed, 23 insertions(+), 42 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs index 54362b276..9cd90b35f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs @@ -117,9 +117,18 @@ private async Task EnsureStatusAccessAsync(string operationType) ApplicationAnalysisOperationType => AIPermissions.Analysis.ViewApplicationAnalysis, AttachmentSummaryOperationType => AIPermissions.Analysis.ViewAttachmentSummary, ApplicationScoringOperationType => AIPermissions.Analysis.ViewScoringResult, + AIGenerationRequestKeyHelper.PipelineOperationType => null, _ => throw new UserFriendlyException($"Unsupported AI generation operation type: {operationType}") }; + if (permission is null) + { + await CheckPolicyAsync(AIPermissions.Analysis.ViewApplicationAnalysis); + await CheckPolicyAsync(AIPermissions.Analysis.ViewAttachmentSummary); + await CheckPolicyAsync(AIPermissions.Analysis.ViewScoringResult); + return; + } + await CheckPolicyAsync(permission); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index b68bc63b9..cc9c902bd 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -482,10 +482,7 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { globalThis.AIGenerationButtonState?.setGenerating($button); - abp.ajax({ - url: `/api/app/ai/generation/application-analysis?applicationId=${encodeURIComponent(applicationId)}`, - type: 'POST' - }) + unity.ai.generation.aIGeneration.generateApplicationAnalysis(applicationId) .done(function(generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); @@ -514,10 +511,7 @@ function monitorAIAnalysisGeneration(applicationId, $button, existingHtml) { aiAnalysisMonitor = globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: existingHtml, - getStatus: () => abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=application-analysis`, - type: 'GET' - }), + getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis'), onComplete: loadAIAnalysis, onFailed: (request) => { loadAIAnalysis(); @@ -574,10 +568,7 @@ $(function() { return; } - abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=application-analysis`, - type: 'GET' - }) + unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis') .done(function(generationStatus) { const request = generationStatus?.generationRequest; if (request?.isActive !== true) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 369eb3dda..1f91eaade 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -686,20 +686,14 @@ function queueApplicationScoring(triggerButton = null) { const monitorScoring = () => globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: existingHtml, - getStatus: () => abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=application-scoring`, - type: 'GET' - }), + getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-scoring'), onComplete: () => { PubSub.publish('refresh_assessment_scores', null); }, onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); - abp.ajax({ - url: `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(applicationId)}`, - type: 'POST' - }) + unity.ai.generation.aIGeneration.generateApplicationScoring(applicationId) .done(function (generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 72eb1bdad..a0fa62d82 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -238,10 +238,7 @@ $(function () { globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: originalHtml ?? $button.html(), - getStatus: () => abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=attachment-summary`, - type: 'GET' - }), + getStatus: () => globalThis.AIGenerationApi.getStatus(applicationId, 'attachment-summary'), onComplete: refreshAttachmentSummaryResults, onFailed: (request) => { abp.message.error(request?.failureReason || 'AI attachment summary generation failed.'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 5ce010714..5b75d3617 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -471,10 +471,7 @@ function generateAiButtonAction(e, dt, button, config) { globalThis.AIGenerationButtonState?.setGenerating($button); } - abp.ajax({ - url: `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(pageApplicationId)}`, - type: 'POST' - }) + unity.ai.generation.aIGeneration.generateApplicationScoring(pageApplicationId) .done(function (generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); @@ -520,10 +517,7 @@ function resumeActiveReviewListAiButton(reviewListTable) { } const $button = $(button.node()); - abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(pageApplicationId)}&operationType=application-scoring`, - type: 'GET' - }) + unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring') .done(function(generationStatus) { const request = generationStatus?.generationRequest; if (request?.isActive !== true) { @@ -539,10 +533,7 @@ function pollReviewListAiButton($button) { globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: generateAiButtonText(null, null, null), - getStatus: () => abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(pageApplicationId)}&operationType=application-scoring`, - type: 'GET' - }), + getStatus: () => unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring'), onComplete: refreshReviewListAfterAiScoring, onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs index 8b97e9876..08d98e400 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Unity.AI.Automation; +using Unity.AI.Domain; using Unity.AI.Generation; using Unity.AI.Localization; using Unity.AI.Operations; @@ -12,10 +13,10 @@ using Unity.AI.Settings; using Unity.GrantManager.GrantApplications; using Unity.GrantManager.GrantApplications.Automation.BackgroundJobs; -using Volo.Abp.EventBus.Local; -using Volo.Abp.Features; using Volo.Abp; using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; +using Volo.Abp.Features; using Volo.Abp.MultiTenancy; using Xunit; using Xunit.Abstractions; @@ -40,7 +41,7 @@ public async Task GenerateAttachmentSummariesAsync_Should_Validate_Against_Appli Substitute.For(), Substitute.For(), featureGuard, - Substitute.For()); + Substitute.For()); service.LazyServiceProvider = GetRequiredService(); var result = await service.GenerateAttachmentSummariesAsync(new GenerateAttachmentSummariesInputDto @@ -64,7 +65,7 @@ public async Task GetStatusAsync_Should_Map_Request_And_Rate_Limit_State() var requestId = Guid.NewGuid(); var statusService = Substitute.For(); - statusService.GetLatestAsync(applicationId, operationType, tenantId).Returns(new Unity.GrantManager.GrantApplications.AIGenerationRequestDto + statusService.GetLatestAsync(applicationId, operationType, tenantId).Returns(new AIGenerationRequestDto { Id = requestId, ApplicationId = applicationId, @@ -134,6 +135,4 @@ private static AIFeatureGuard CreateFeatureGuard() var localizer = Substitute.For>(); return new AIFeatureGuard(featureChecker, localizer); } - } - From d94c554857f1e5056196617d2c6062e4b3c8b83c Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 8 Jul 2026 17:44:09 -0700 Subject: [PATCH 06/22] AB#33756 fix ai button state flicker --- .../Pages/GrantApplications/ai-analysis.js | 19 ++++---- .../ai-generation-button-state.js | 6 +-- .../AssessmentScoresWidget/Default.js | 1 - .../ChefsAttachments/ChefsAttachments.js | 10 +--- .../Components/ReviewList/ReviewList.js | 21 ++++---- .../Automation/AIGenerationAppServiceTests.cs | 1 - .../GrantManagerApplicationTestBase.cs | 48 +++++++++---------- 7 files changed, 45 insertions(+), 61 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index cc9c902bd..23cdf3ab6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -568,16 +568,15 @@ $(function() { return; } - unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis') - .done(function(generationStatus) { - const request = generationStatus?.generationRequest; - if (request?.isActive !== true) { - return; - } + unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis').done(function(generationStatus) { + const request = generationStatus?.generationRequest; + if (request?.isActive !== true) { + return; + } - const existingHtml = $regenerateButton.html(); - globalThis.AIGenerationButtonState?.setGenerating($regenerateButton); - monitorAIAnalysisGeneration(applicationId, $regenerateButton, existingHtml); - }); + const existingHtml = $regenerateButton.html(); + globalThis.AIGenerationButtonState?.setGenerating($regenerateButton); + monitorAIAnalysisGeneration(applicationId, $regenerateButton, existingHtml); + }); } }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index c11b0c1ee..95d50db94 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -14,10 +14,9 @@ } function applyRateLimitState(generationStatus, options = {}) { - const request = generationStatus?.generationRequest; global.applyAIRateLimitState?.( { - isGenerating: generationStatus?.isGenerating === true || request?.isActive === true, + isGenerating: generationStatus?.isGenerating === true, retryAfterSeconds: Number(generationStatus?.retryAfterSeconds) || 0 }, { pollWhenGenerating: options.pollWhenGenerating === true } @@ -76,7 +75,6 @@ options.onFailed?.(request); return; } - if (!request) { stop(); restoreButton(options.$button, options.originalHtml); @@ -85,7 +83,7 @@ return; } - if (request.isActive === false || status === 'Completed') { + if (generationStatus?.isGenerating !== true) { stop(); restoreButtonForCooldownCheck(options.$button, options.originalHtml); applyRateLimitState(generationStatus, { pollWhenGenerating: true }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 1f91eaade..1d4a17fe4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -704,7 +704,6 @@ function queueApplicationScoring(triggerButton = null) { PubSub.publish('refresh_assessment_scores', null); return; } - monitorScoring(); }) .fail(function () { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index a0fa62d82..1428706d0 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -227,18 +227,10 @@ $(function () { } function pollAttachmentSummaryGeneration(applicationId, $button, originalHtml) { - if (!globalThis.AIGenerationButtonState?.monitor) { - console.error('AIGenerationButtonState is not available; cannot poll attachment summary generation.'); - abp.message.error('AI attachment summary polling is unavailable. Please refresh and try again.'); - globalThis.AIGenerationButtonState?.restore($button); - $button.html(originalHtml ?? $button.html()).prop('disabled', false); - return; - } - globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: originalHtml ?? $button.html(), - getStatus: () => globalThis.AIGenerationApi.getStatus(applicationId, 'attachment-summary'), + getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'attachment-summary'), onComplete: refreshAttachmentSummaryResults, onFailed: (request) => { abp.message.error(request?.failureReason || 'AI attachment summary generation failed.'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 5b75d3617..f961de251 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -482,15 +482,13 @@ function generateAiButtonAction(e, dt, button, config) { refreshReviewListAfterAiScoring(); return; } - pollReviewListAiButton($button); }) .fail(function () { abp.message.error('Failed to queue AI scoring. Please try again.'); restoreReviewListAiButton($button); globalThis.syncAIRateLimitButtons?.(); - }) - ; + }); } function restoreReviewListAiButton($button) { @@ -517,16 +515,15 @@ function resumeActiveReviewListAiButton(reviewListTable) { } const $button = $(button.node()); - unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring') - .done(function(generationStatus) { - const request = generationStatus?.generationRequest; - if (request?.isActive !== true) { - return; - } + unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring').done(function(generationStatus) { + const request = generationStatus?.generationRequest; + if (request?.isActive !== true) { + return; + } - globalThis.AIGenerationButtonState?.setGenerating($button); - pollReviewListAiButton($button); - }); + globalThis.AIGenerationButtonState?.setGenerating($button); + pollReviewListAiButton($button); + }); } function pollReviewListAiButton($button) { diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs index 08d98e400..c52e06b90 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Unity.AI.Automation; using Unity.AI.Domain; -using Unity.AI.Generation; using Unity.AI.Localization; using Unity.AI.Operations; using Unity.AI.RateLimit; diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs index 138320e81..e0ab7d0c1 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs @@ -1,35 +1,35 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; using System; using System.Threading.Tasks; using Volo.Abp.Features; using Volo.Abp.Users; -using Xunit; -using Xunit.Abstractions; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace Unity.GrantManager; - +using Xunit; +using Xunit.Abstractions; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Unity.GrantManager; + public abstract class GrantManagerApplicationTestBase : GrantManagerTestBase, IAsyncLifetime { protected ICurrentUser? _currentUser; protected IFeatureChecker? _featureChecker; - - protected GrantManagerApplicationTestBase(ITestOutputHelper _) - { - } - - public virtual async Task InitializeAsync() - { - await Task.CompletedTask; - } - - public virtual async Task DisposeAsync() - { - await Task.Delay(15); - } - + + protected GrantManagerApplicationTestBase(ITestOutputHelper _) + { + } + + public virtual async Task InitializeAsync() + { + await Task.CompletedTask; + } + + public virtual async Task DisposeAsync() + { + await Task.Delay(15); + } + protected override void AfterAddApplication(IServiceCollection services) { _currentUser = Substitute.For(); @@ -38,7 +38,7 @@ protected override void AfterAddApplication(IServiceCollection services) services.AddSingleton(_currentUser); services.AddSingleton(_featureChecker); } - + protected void Login(Guid userId) { _currentUser?.Id.Returns(userId); From 807b5f05b06fb2be44353656599021e80759a349 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 08:37:35 -0700 Subject: [PATCH 07/22] AB#33756 fix Status enum-to-string mapping in AI generation status Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/AIGenerationRequestDto.cs | 2 ++ .../Generation/AIGenerationStatusDto.cs | 2 ++ .../Generation/AIGenerationAppService.cs | 1 + .../Pages/GrantApplications/ai-analysis.js | 4 ---- .../GrantApplications/ai-generation-button-state.js | 13 +++++++++++++ .../Components/AssessmentScoresWidget/Default.js | 5 ++++- .../Components/ChefsAttachments/ChefsAttachments.js | 5 +++-- .../Shared/Components/ReviewList/ReviewList.js | 5 ++++- .../Automation/AIGenerationAppServiceTests.cs | 1 + 9 files changed, 30 insertions(+), 8 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs index febc00ed3..3a832df03 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs @@ -9,4 +9,6 @@ public class AIGenerationRequestDto public Guid OperationId { get; set; } public string OperationType { get; set; } = string.Empty; + + public string Status { get; set; } = string.Empty; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationStatusDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationStatusDto.cs index 2b22d906a..ca5684257 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationStatusDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationStatusDto.cs @@ -4,6 +4,8 @@ public class AIGenerationStatusDto { public AIGenerationStatusRequestDto? GenerationRequest { get; set; } + public string? FailureReason { get; set; } + public bool IsGenerating { get; set; } public int RetryAfterSeconds { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs index 9cd90b35f..443b3602c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs @@ -105,6 +105,7 @@ public virtual async Task GetStatusAsync(Guid application FailureReason = request.FailureReason, IsActive = request.IsActive }, + FailureReason = request?.FailureReason, IsGenerating = state.IsGenerating, RetryAfterSeconds = state.RetryAfterSeconds }; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index 23cdf3ab6..71892b9bc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -513,10 +513,6 @@ function monitorAIAnalysisGeneration(applicationId, $button, existingHtml) { originalHtml: existingHtml, getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis'), onComplete: loadAIAnalysis, - onFailed: (request) => { - loadAIAnalysis(); - abp.message.error(request?.failureReason || 'AI analysis failed.'); - }, onPollFailed: (error) => { console.warn('Failed to poll AI analysis status.', error); abp.message.error('Unable to load AI analysis status. Please try again.'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 95d50db94..9ba410df1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -23,6 +23,11 @@ ); } + function isFailedRequest(generationStatus) { + const request = generationStatus?.generationRequest; + return request?.status === 'Failed'; + } + global.AIGenerationButtonState = { setGenerating($button) { global.setAIGenerationButtonsGenerating?.({ poll: false }); @@ -83,6 +88,14 @@ return; } + if (isFailedRequest(generationStatus)) { + stop(); + restoreButton(options.$button, options.originalHtml); + applyRateLimitState(generationStatus, { pollWhenGenerating: true }); + options.onPollFailed?.(new Error(generationStatus?.failureReason || 'AI generation failed.')); + return; + } + if (generationStatus?.isGenerating !== true) { stop(); restoreButtonForCooldownCheck(options.$button, options.originalHtml); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 1d4a17fe4..67fdcb5fc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -690,7 +690,10 @@ function queueApplicationScoring(triggerButton = null) { onComplete: () => { PubSub.publish('refresh_assessment_scores', null); }, - onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') + onPollFailed: (error) => { + console.warn('Failed to poll AI scoring status.', error); + abp.message.error(error?.message || 'AI scoring failed.'); + } }); unity.ai.generation.aIGeneration.generateApplicationScoring(applicationId) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 1428706d0..1c931e92e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -232,8 +232,9 @@ $(function () { originalHtml: originalHtml ?? $button.html(), getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'attachment-summary'), onComplete: refreshAttachmentSummaryResults, - onFailed: (request) => { - abp.message.error(request?.failureReason || 'AI attachment summary generation failed.'); + onPollFailed: (error) => { + console.warn('Failed to poll AI attachment summary status.', error); + abp.message.error(error?.message || 'AI attachment summary generation failed.'); } }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index f961de251..034d1ab58 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -532,7 +532,10 @@ function pollReviewListAiButton($button) { originalHtml: generateAiButtonText(null, null, null), getStatus: () => unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring'), onComplete: refreshReviewListAfterAiScoring, - onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') + onPollFailed: (error) => { + console.warn('Failed to poll AI scoring status.', error); + abp.message.error(error?.message || 'AI scoring failed.'); + } }); } diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs index c52e06b90..237df9ab8 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs @@ -7,6 +7,7 @@ using Unity.AI.Automation; using Unity.AI.Domain; using Unity.AI.Localization; +using Unity.AI.Generation; using Unity.AI.Operations; using Unity.AI.RateLimit; using Unity.AI.Settings; From bde3c00a324420f7aa5c0c6516d099b17d29c379 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 08:49:52 -0700 Subject: [PATCH 08/22] AB#33756 remove stray test base churn --- .../GrantManagerApplicationTestBase.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs index e0ab7d0c1..138320e81 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs @@ -1,35 +1,35 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; using System; using System.Threading.Tasks; using Volo.Abp.Features; using Volo.Abp.Users; -using Xunit; -using Xunit.Abstractions; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace Unity.GrantManager; - +using Xunit; +using Xunit.Abstractions; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Unity.GrantManager; + public abstract class GrantManagerApplicationTestBase : GrantManagerTestBase, IAsyncLifetime { protected ICurrentUser? _currentUser; protected IFeatureChecker? _featureChecker; - - protected GrantManagerApplicationTestBase(ITestOutputHelper _) - { - } - - public virtual async Task InitializeAsync() - { - await Task.CompletedTask; - } - - public virtual async Task DisposeAsync() - { - await Task.Delay(15); - } - + + protected GrantManagerApplicationTestBase(ITestOutputHelper _) + { + } + + public virtual async Task InitializeAsync() + { + await Task.CompletedTask; + } + + public virtual async Task DisposeAsync() + { + await Task.Delay(15); + } + protected override void AfterAddApplication(IServiceCollection services) { _currentUser = Substitute.For(); @@ -38,7 +38,7 @@ protected override void AfterAddApplication(IServiceCollection services) services.AddSingleton(_currentUser); services.AddSingleton(_featureChecker); } - + protected void Login(Guid userId) { _currentUser?.Id.Returns(userId); From b3b8d14ae502a9bfbd1f5ff7801581e31fafeb92 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 12:09:51 -0700 Subject: [PATCH 09/22] AB#33756 fix AI button polling state --- .../Pages/GrantApplications/ai-analysis.js | 4 ++++ .../Pages/GrantApplications/ai-generation-button-state.js | 2 +- .../Shared/Components/AssessmentScoresWidget/Default.js | 5 +---- .../Shared/Components/ChefsAttachments/ChefsAttachments.js | 3 +++ .../Views/Shared/Components/ReviewList/ReviewList.js | 5 +---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index 71892b9bc..23cdf3ab6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -513,6 +513,10 @@ function monitorAIAnalysisGeneration(applicationId, $button, existingHtml) { originalHtml: existingHtml, getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis'), onComplete: loadAIAnalysis, + onFailed: (request) => { + loadAIAnalysis(); + abp.message.error(request?.failureReason || 'AI analysis failed.'); + }, onPollFailed: (error) => { console.warn('Failed to poll AI analysis status.', error); abp.message.error('Unable to load AI analysis status. Please try again.'); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 9ba410df1..77fec92c6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -96,7 +96,7 @@ return; } - if (generationStatus?.isGenerating !== true) { + if (request.isActive !== true) { stop(); restoreButtonForCooldownCheck(options.$button, options.originalHtml); applyRateLimitState(generationStatus, { pollWhenGenerating: true }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 67fdcb5fc..1d4a17fe4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -690,10 +690,7 @@ function queueApplicationScoring(triggerButton = null) { onComplete: () => { PubSub.publish('refresh_assessment_scores', null); }, - onPollFailed: (error) => { - console.warn('Failed to poll AI scoring status.', error); - abp.message.error(error?.message || 'AI scoring failed.'); - } + onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); unity.ai.generation.aIGeneration.generateApplicationScoring(applicationId) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 1c931e92e..c1fd386b6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -211,6 +211,9 @@ $(function () { contentType: 'application/json', type: 'POST', success: function (generationStatus) { + if (generationStatus?.generationRequest?.isActive !== true) { + return; + } globalThis.AIGenerationButtonState?.setGenerating($activeButton); pollAttachmentSummaryGeneration(applicationId, $activeButton, existingHTML); }, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index 034d1ab58..f961de251 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -532,10 +532,7 @@ function pollReviewListAiButton($button) { originalHtml: generateAiButtonText(null, null, null), getStatus: () => unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring'), onComplete: refreshReviewListAfterAiScoring, - onPollFailed: (error) => { - console.warn('Failed to poll AI scoring status.', error); - abp.message.error(error?.message || 'AI scoring failed.'); - } + onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); } From 507dac7fe853ed9ad4b6322fb55a3d9b9cc97947 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 12:18:34 -0700 Subject: [PATCH 10/22] AB#33756 restore attachment summary helper --- .../AI/Operations/AttachmentSummaryService.cs | 10 ++++++ .../ChefsAttachments/ChefsAttachments.js | 3 -- .../AttachmentSummaryServiceTests.cs | 33 +------------------ 3 files changed, 11 insertions(+), 35 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs index 0f5a43728..8dd229270 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -171,6 +171,16 @@ private async Task> GenerateBatchAsync( return results; } + private async Task LoadAttachmentAsync(Guid attachmentId) + { + return await attachmentSummaryPersistence.LoadAsync(attachmentId); + } + + private async Task SaveSummaryAsync(Guid attachmentId, string summary) + { + await attachmentSummaryPersistence.SaveSummaryAsync(attachmentId, summary); + } + private async Task> GenerateSequentiallyAsync( IReadOnlyCollection attachmentIds, string? promptVersion, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index c1fd386b6..1c931e92e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -211,9 +211,6 @@ $(function () { contentType: 'application/json', type: 'POST', success: function (generationStatus) { - if (generationStatus?.generationRequest?.isActive !== true) { - return; - } globalThis.AIGenerationButtonState?.setGenerating($activeButton); pollAttachmentSummaryGeneration(applicationId, $activeButton, existingHTML); }, diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index dad6869bc..ca1af9240 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs @@ -2,12 +2,10 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; -using NPOI.XWPF.UserModel; using Shouldly; using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -17,7 +15,7 @@ using Unity.AI.Operations; using Unity.AI.Requests; using Unity.AI.Responses; -using Unity.GrantManager.Intakes; +using Unity.GrantManager.Applications; using Volo.Abp; using Volo.Abp.Uow; using Xunit; @@ -94,35 +92,6 @@ public async Task GenerateAndSaveAsync_Should_Reject_Empty_Attachment_List() await Should.ThrowAsync(() => service.GenerateAndSaveAsync([], "v1")); } - [Fact] - public async Task GenerateAndSaveAsync_Should_Not_Call_AI_When_Supported_File_Extraction_Is_Empty() - { - var attachmentId = Guid.NewGuid(); - var submissionId = Guid.NewGuid(); - var fileId = Guid.NewGuid(); - var stream = new MemoryStream([1, 2, 3]); - string? savedSummary = null; - - var provider = CreateProvider(attachmentId, "test.docx", submissionId, fileId, summary => savedSummary = summary); - - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "test.docx") - .Returns(new ChefsFileAttachmentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); - - var textExtractionService = Substitute.For(); - textExtractionService.ExtractTextAsync("test.docx", stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", Arg.Any()) - .Returns(string.Empty); - - var aiService = Substitute.For(); - var service = CreateService(provider, streamProvider, textExtractionService, aiService); - - var result = await service.GenerateAndSaveAsync(attachmentId, "v1"); - - result.ShouldBe("Attachment text could not be extracted for AI summary generation."); - savedSummary.ShouldBe(result); - await aiService.DidNotReceive().GenerateAttachmentSummaryAsync(Arg.Any(), Arg.Any()); - } - [Fact] public async Task GenerateAndSaveAsync_Should_Use_Batch_Mode_When_Configured() { From e2324c927f21080d015d92f54f6a7d6c1954abfa Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 7 Jul 2026 16:01:00 -0700 Subject: [PATCH 11/22] AB#32311 move attachment summary data provider to host app --- .../AttachmentSummaryDataProvider.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs index e0f416dff..8a8ee0bbb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs @@ -1,12 +1,11 @@ -using System; using Microsoft.Extensions.Localization; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Unity.AI.Operations; using Unity.AI.Localization; +using Unity.AI.Operations; using Unity.GrantManager.Applications; -using Volo.Abp; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; @@ -23,16 +22,13 @@ public class AttachmentSummaryDataProvider( var attachment = await applicationChefsFileAttachmentRepository.FindAsync(attachmentId); await uow.CompleteAsync(); - if (attachment == null) - { - return null; - } - - return new AttachmentSummarySource( - attachment.Id, - attachment.FileName, - attachment.ChefsSubmissionId, - attachment.ChefsFileId); + return attachment == null + ? null + : new AttachmentSummarySource( + attachment.Id, + attachment.FileName, + attachment.ChefsSubmissionId, + attachment.ChefsFileId); } public async Task UpdateAttachmentSummaryAsync(Guid attachmentId, string summary) From e4de5af7dd22f4b2713e354e77cbf5c7c6b4f0e9 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 7 Jul 2026 17:38:20 -0700 Subject: [PATCH 12/22] AB#32311 fix AI attachment summary tests --- .../AI/Operations/AttachmentSummaryServiceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index ca1af9240..8d104e26f 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; using Unity.AI; From 2d70ae502009dae6ab295f67bebe4b9afea5e871 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 8 Jul 2026 15:59:07 -0700 Subject: [PATCH 13/22] AB#33756 clean up AI route surface --- .../Pages/GrantApplications/ai-analysis.js | 1 - .../GrantApplications/Automation/AIGenerationAppServiceTests.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index 23cdf3ab6..b3b92acf6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -573,7 +573,6 @@ $(function() { if (request?.isActive !== true) { return; } - const existingHtml = $regenerateButton.html(); globalThis.AIGenerationButtonState?.setGenerating($regenerateButton); monitorAIAnalysisGeneration(applicationId, $regenerateButton, existingHtml); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs index 237df9ab8..02f0ca71c 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Unity.AI.Automation; using Unity.AI.Domain; +using Unity.AI.Generation; using Unity.AI.Localization; using Unity.AI.Generation; using Unity.AI.Operations; From b1f6d7e3875079fdc954a9ab684c242917cc948a Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 8 Jul 2026 17:44:09 -0700 Subject: [PATCH 14/22] AB#33756 fix ai button state flicker --- .../AssessmentScoresWidget/Default.js | 10 +++- .../ChefsAttachments/ChefsAttachments.js | 5 +- .../Automation/AIGenerationAppServiceTests.cs | 1 - .../GrantManagerApplicationTestBase.cs | 48 +++++++++---------- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 1d4a17fe4..512540921 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -686,14 +686,20 @@ function queueApplicationScoring(triggerButton = null) { const monitorScoring = () => globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: existingHtml, - getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-scoring'), + getStatus: () => abp.ajax({ + url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=application-scoring`, + type: 'GET' + }), onComplete: () => { PubSub.publish('refresh_assessment_scores', null); }, onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); - unity.ai.generation.aIGeneration.generateApplicationScoring(applicationId) + abp.ajax({ + url: `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(applicationId)}`, + type: 'POST' + }) .done(function (generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 1c931e92e..39f1b5be4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -230,7 +230,10 @@ $(function () { globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: originalHtml ?? $button.html(), - getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'attachment-summary'), + getStatus: () => abp.ajax({ + url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=attachment-summary`, + type: 'GET' + }), onComplete: refreshAttachmentSummaryResults, onPollFailed: (error) => { console.warn('Failed to poll AI attachment summary status.', error); diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs index 02f0ca71c..237df9ab8 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Unity.AI.Automation; using Unity.AI.Domain; -using Unity.AI.Generation; using Unity.AI.Localization; using Unity.AI.Generation; using Unity.AI.Operations; diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs index 138320e81..e0ab7d0c1 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantManagerApplicationTestBase.cs @@ -1,35 +1,35 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using NSubstitute; using System; using System.Threading.Tasks; using Volo.Abp.Features; using Volo.Abp.Users; -using Xunit; -using Xunit.Abstractions; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace Unity.GrantManager; - +using Xunit; +using Xunit.Abstractions; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Unity.GrantManager; + public abstract class GrantManagerApplicationTestBase : GrantManagerTestBase, IAsyncLifetime { protected ICurrentUser? _currentUser; protected IFeatureChecker? _featureChecker; - - protected GrantManagerApplicationTestBase(ITestOutputHelper _) - { - } - - public virtual async Task InitializeAsync() - { - await Task.CompletedTask; - } - - public virtual async Task DisposeAsync() - { - await Task.Delay(15); - } - + + protected GrantManagerApplicationTestBase(ITestOutputHelper _) + { + } + + public virtual async Task InitializeAsync() + { + await Task.CompletedTask; + } + + public virtual async Task DisposeAsync() + { + await Task.Delay(15); + } + protected override void AfterAddApplication(IServiceCollection services) { _currentUser = Substitute.For(); @@ -38,7 +38,7 @@ protected override void AfterAddApplication(IServiceCollection services) services.AddSingleton(_currentUser); services.AddSingleton(_featureChecker); } - + protected void Login(Guid userId) { _currentUser?.Id.Returns(userId); From 9de25856d80de734c12490edeb2cf4a1f9f7db30 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 15:23:48 -0700 Subject: [PATCH 15/22] AB#33756 resolve rebase cleanup issues --- .../ai-generation-button-state.js | 1 + .../AssessmentScoresWidget/Default.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 77fec92c6..9a073fde8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -77,6 +77,7 @@ stop(); restoreButton(options.$button, options.originalHtml); applyRateLimitState(generationStatus, { pollWhenGenerating: true }); + options.onPollFailed?.(new Error(generationStatus?.failureReason || 'AI generation failed.')); options.onFailed?.(request); return; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 512540921..6e2e03a0e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -744,4 +744,21 @@ $(function () { $(document).on('click', '[id^="scoresheet-section-discard-"]', function () { discardChangesScoresSection($(this).data('form-id'), $(this).data('section-id')); }); + + $(document).on('click', '#btn-expand-all', function () { + expandAllAccordions('assessment-scoresheet'); + }); + $(document).on('click', '#btn-collapse-all', function () { + collapseAllAccordions('assessment-scoresheet'); + }); + $(document).on('click', '#saveAssessmentScoresBtn', function () { + saveAssessmentScores(); + }); + + $(document).on('click', '[id^="scoresheet-section-save-"]', function () { + saveScoresSection($(this).data('form-id'), $(this).data('section-id')); + }); + $(document).on('click', '[id^="scoresheet-section-discard-"]', function () { + discardChangesScoresSection($(this).data('form-id'), $(this).data('section-id')); + }); }); From 2954618cc27c0df6c856740b28a46a1a4111c6b7 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 15:33:51 -0700 Subject: [PATCH 16/22] AB#33756 fix attachment summary and failure reason --- .../AI/Operations/AttachmentSummaryService.cs | 10 ---------- .../GrantApplications/GrantApplicationAppService.cs | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs index 8dd229270..0f5a43728 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -171,16 +171,6 @@ private async Task> GenerateBatchAsync( return results; } - private async Task LoadAttachmentAsync(Guid attachmentId) - { - return await attachmentSummaryPersistence.LoadAsync(attachmentId); - } - - private async Task SaveSummaryAsync(Guid attachmentId, string summary) - { - await attachmentSummaryPersistence.SaveSummaryAsync(attachmentId, summary); - } - private async Task> GenerateSequentiallyAsync( IReadOnlyCollection attachmentIds, string? promptVersion, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs index 2078de5b0..52adccaaf 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/GrantApplicationAppService.cs @@ -1373,7 +1373,8 @@ public async Task GetAIGenerationStatusAsync(Guid applica { GenerationRequest = request, IsGenerating = state.IsGenerating, - RetryAfterSeconds = state.RetryAfterSeconds + RetryAfterSeconds = state.RetryAfterSeconds, + FailureReason = request?.FailureReason }; } From 00869412e32c9b8eaa50a5974260cf89e4056b6b Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 15:36:56 -0700 Subject: [PATCH 17/22] AB#33756 add failure reason to ai status dto --- .../GrantApplications/AIGenerationStatusDto.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs index 55814b6be..f229885d7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/AIGenerationStatusDto.cs @@ -3,6 +3,7 @@ namespace Unity.GrantManager.GrantApplications; public class AIGenerationStatusDto { public AIGenerationRequestDto? GenerationRequest { get; set; } + public string? FailureReason { get; set; } public bool IsGenerating { get; set; } public int RetryAfterSeconds { get; set; } } From b98dda2c1fa0d02e8dc985a50bb80d0a4fa68f47 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 15:56:02 -0700 Subject: [PATCH 18/22] AB#33756 remove dead ai poll cleanup --- .../GrantApplications/ai-generation-button-state.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 9a073fde8..cdfa9c496 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -23,11 +23,6 @@ ); } - function isFailedRequest(generationStatus) { - const request = generationStatus?.generationRequest; - return request?.status === 'Failed'; - } - global.AIGenerationButtonState = { setGenerating($button) { global.setAIGenerationButtonsGenerating?.({ poll: false }); @@ -89,14 +84,6 @@ return; } - if (isFailedRequest(generationStatus)) { - stop(); - restoreButton(options.$button, options.originalHtml); - applyRateLimitState(generationStatus, { pollWhenGenerating: true }); - options.onPollFailed?.(new Error(generationStatus?.failureReason || 'AI generation failed.')); - return; - } - if (request.isActive !== true) { stop(); restoreButtonForCooldownCheck(options.$button, options.originalHtml); From 036fa310102b8454968f78f8aea0e9372271690e Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 16:10:30 -0700 Subject: [PATCH 19/22] AB#33756 centralize ai web requests --- .../Pages/GrantApplications/ai-analysis.js | 9 ++-- .../ai-generation-button-state.js | 41 +++++++++++++++++++ .../AssessmentScoresWidget/Default.js | 10 +---- .../ChefsAttachments/ChefsAttachments.js | 26 ++++-------- .../Components/ReviewList/ReviewList.js | 9 ++-- 5 files changed, 60 insertions(+), 35 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index b3b92acf6..84cfa847d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js @@ -482,7 +482,7 @@ globalThis.queueApplicationAnalysis = function(triggerButton = null) { globalThis.AIGenerationButtonState?.setGenerating($button); - unity.ai.generation.aIGeneration.generateApplicationAnalysis(applicationId) + globalThis.AIGenerationApi.queueApplicationAnalysis(applicationId) .done(function(generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); @@ -511,7 +511,7 @@ function monitorAIAnalysisGeneration(applicationId, $button, existingHtml) { aiAnalysisMonitor = globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: existingHtml, - getStatus: () => unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis'), + getStatus: () => globalThis.AIGenerationApi.getStatus(applicationId, 'application-analysis'), onComplete: loadAIAnalysis, onFailed: (request) => { loadAIAnalysis(); @@ -568,9 +568,8 @@ $(function() { return; } - unity.ai.generation.aIGeneration.getStatus(applicationId, 'application-analysis').done(function(generationStatus) { - const request = generationStatus?.generationRequest; - if (request?.isActive !== true) { + globalThis.AIGenerationApi.getStatus(applicationId, 'application-analysis').done(function(generationStatus) { + if (generationStatus?.generationRequest?.isActive !== true) { return; } const existingHtml = $regenerateButton.html(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index cdfa9c496..36e70171d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -1,4 +1,16 @@ (function (global) { + function request(url, type, data = null, contentType = null) { + const options = { url, type }; + if (data !== null) { + options.data = data; + } + if (contentType) { + options.contentType = contentType; + } + + return abp.ajax(options); + } + function restoreButton($button, html) { global.AIGenerationButtonState.restore($button); $button.html(html).prop('disabled', false); @@ -113,4 +125,33 @@ return { stop }; }, }; + + global.AIGenerationApi = { + queueApplicationAnalysis(applicationId) { + return request( + `/api/app/ai/generation/application-analysis?applicationId=${encodeURIComponent(applicationId)}`, + 'POST' + ); + }, + queueApplicationScoring(applicationId) { + return request( + `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(applicationId)}`, + 'POST' + ); + }, + queueAttachmentSummary(input) { + return request( + '/api/app/ai/generation/attachment-summary', + 'POST', + JSON.stringify(input), + 'application/json' + ); + }, + getStatus(applicationId, operationType) { + return request( + `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=${encodeURIComponent(operationType)}`, + 'GET' + ); + }, + }; })(globalThis); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 6e2e03a0e..8414d6c9d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -686,20 +686,14 @@ function queueApplicationScoring(triggerButton = null) { const monitorScoring = () => globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: existingHtml, - getStatus: () => abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=application-scoring`, - type: 'GET' - }), + getStatus: () => globalThis.AIGenerationApi.getStatus(applicationId, 'application-scoring'), onComplete: () => { PubSub.publish('refresh_assessment_scores', null); }, onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); - abp.ajax({ - url: `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(applicationId)}`, - type: 'POST' - }) + globalThis.AIGenerationApi.queueApplicationScoring(applicationId) .done(function (generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 39f1b5be4..61f25e632 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -202,27 +202,22 @@ $(function () { ) .prop('disabled', true); - $.ajax({ - url: '/api/app/ai/generation/attachment-summary', - data: JSON.stringify({ - applicationId: applicationId, - attachmentIds: summaryAttachmentIds, - }), - contentType: 'application/json', - type: 'POST', - success: function (generationStatus) { + globalThis.AIGenerationApi.queueAttachmentSummary({ + applicationId: applicationId, + attachmentIds: summaryAttachmentIds, + }) + .done(function (generationStatus) { globalThis.AIGenerationButtonState?.setGenerating($activeButton); pollAttachmentSummaryGeneration(applicationId, $activeButton, existingHTML); - }, - error: function (error) { + }) + .fail(function (error) { console.error('Error generating AI summaries:', error); abp.message.error('An error occurred while generating AI summaries. Please try again.'); globalThis.AIGenerationButtonState?.restore($activeButton); globalThis.refreshAIRateLimitState?.(); $activeButton.html(existingHTML).prop('disabled', false); setGenerateSummariesEnabled(); - }, - }); + }); }); } @@ -230,10 +225,7 @@ $(function () { globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: originalHtml ?? $button.html(), - getStatus: () => abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=attachment-summary`, - type: 'GET' - }), + getStatus: () => globalThis.AIGenerationApi.getStatus(applicationId, 'attachment-summary'), onComplete: refreshAttachmentSummaryResults, onPollFailed: (error) => { console.warn('Failed to poll AI attachment summary status.', error); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js index f961de251..e1d5a5056 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js @@ -471,7 +471,7 @@ function generateAiButtonAction(e, dt, button, config) { globalThis.AIGenerationButtonState?.setGenerating($button); } - unity.ai.generation.aIGeneration.generateApplicationScoring(pageApplicationId) + globalThis.AIGenerationApi.queueApplicationScoring(pageApplicationId) .done(function (generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); @@ -515,9 +515,8 @@ function resumeActiveReviewListAiButton(reviewListTable) { } const $button = $(button.node()); - unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring').done(function(generationStatus) { - const request = generationStatus?.generationRequest; - if (request?.isActive !== true) { + globalThis.AIGenerationApi.getStatus(pageApplicationId, 'application-scoring').done(function(generationStatus) { + if (generationStatus?.generationRequest?.isActive !== true) { return; } @@ -530,7 +529,7 @@ function pollReviewListAiButton($button) { globalThis.AIGenerationButtonState.monitor({ $button, originalHtml: generateAiButtonText(null, null, null), - getStatus: () => unity.ai.generation.aIGeneration.getStatus(pageApplicationId, 'application-scoring'), + getStatus: () => globalThis.AIGenerationApi.getStatus(pageApplicationId, 'application-scoring'), onComplete: refreshReviewListAfterAiScoring, onFailed: (request) => abp.message.error(request?.failureReason || 'AI scoring failed.') }); From 44fc79b1452e577d7926087c3eae9c304905cc2d Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 16:14:48 -0700 Subject: [PATCH 20/22] AB#33756 split ai generation api helper --- .../Pages/GrantApplications/Details.cshtml | 11 ++--- .../GrantApplications/ai-generation-api.js | 42 +++++++++++++++++++ .../ai-generation-button-state.js | 40 ------------------ 3 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-api.js diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index 054f3845c..a0814ebba 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -55,11 +55,12 @@ } -@section scripts -{ - - - +@section scripts +{ + + + + } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-api.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-api.js new file mode 100644 index 000000000..e9ea11fea --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-api.js @@ -0,0 +1,42 @@ +(function (global) { + function request(url, type, data = null, contentType = null) { + const options = { url, type }; + if (data !== null) { + options.data = data; + } + if (contentType) { + options.contentType = contentType; + } + + return abp.ajax(options); + } + + global.AIGenerationApi = { + queueApplicationAnalysis(applicationId) { + return request( + `/api/app/ai/generation/application-analysis?applicationId=${encodeURIComponent(applicationId)}`, + 'POST' + ); + }, + queueApplicationScoring(applicationId) { + return request( + `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(applicationId)}`, + 'POST' + ); + }, + queueAttachmentSummary(input) { + return request( + '/api/app/ai/generation/attachment-summary', + 'POST', + JSON.stringify(input), + 'application/json' + ); + }, + getStatus(applicationId, operationType) { + return request( + `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=${encodeURIComponent(operationType)}`, + 'GET' + ); + }, + }; +})(globalThis); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index 36e70171d..dd9bf0bef 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -1,16 +1,4 @@ (function (global) { - function request(url, type, data = null, contentType = null) { - const options = { url, type }; - if (data !== null) { - options.data = data; - } - if (contentType) { - options.contentType = contentType; - } - - return abp.ajax(options); - } - function restoreButton($button, html) { global.AIGenerationButtonState.restore($button); $button.html(html).prop('disabled', false); @@ -126,32 +114,4 @@ }, }; - global.AIGenerationApi = { - queueApplicationAnalysis(applicationId) { - return request( - `/api/app/ai/generation/application-analysis?applicationId=${encodeURIComponent(applicationId)}`, - 'POST' - ); - }, - queueApplicationScoring(applicationId) { - return request( - `/api/app/ai/generation/application-scoring?applicationId=${encodeURIComponent(applicationId)}`, - 'POST' - ); - }, - queueAttachmentSummary(input) { - return request( - '/api/app/ai/generation/attachment-summary', - 'POST', - JSON.stringify(input), - 'application/json' - ); - }, - getStatus(applicationId, operationType) { - return request( - `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=${encodeURIComponent(operationType)}`, - 'GET' - ); - }, - }; })(globalThis); From e882bd2ac2e6d4a7bc7226b90ac2ce6bed96c034 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 16:48:09 -0700 Subject: [PATCH 21/22] AB#33756 fix ai attachment summary provider tests --- .../Automation/AttachmentSummaryDataProvider.cs | 1 + .../AI/Operations/AttachmentSummaryServiceTests.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs index 8a8ee0bbb..12c8b8f0d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs @@ -8,6 +8,7 @@ using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; +using Volo.Abp; namespace Unity.GrantManager.GrantApplications.Automation; diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index 8d104e26f..85510abda 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs @@ -15,6 +15,7 @@ using Unity.AI.Requests; using Unity.AI.Responses; using Unity.GrantManager.Applications; +using Unity.GrantManager.Intakes; using Volo.Abp; using Volo.Abp.Uow; using Xunit; From 60e9a0d93dda9ddecf0bbb65380b83c03e007e2f Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Thu, 9 Jul 2026 16:52:57 -0700 Subject: [PATCH 22/22] AB#33756 remove duplicate ai assessment handlers --- .../Components/AssessmentScoresWidget/Default.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 8414d6c9d..21934f97d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -739,20 +739,4 @@ $(function () { discardChangesScoresSection($(this).data('form-id'), $(this).data('section-id')); }); - $(document).on('click', '#btn-expand-all', function () { - expandAllAccordions('assessment-scoresheet'); - }); - $(document).on('click', '#btn-collapse-all', function () { - collapseAllAccordions('assessment-scoresheet'); - }); - $(document).on('click', '#saveAssessmentScoresBtn', function () { - saveAssessmentScores(); - }); - - $(document).on('click', '[id^="scoresheet-section-save-"]', function () { - saveScoresSection($(this).data('form-id'), $(this).data('section-id')); - }); - $(document).on('click', '[id^="scoresheet-section-discard-"]', function () { - discardChangesScoresSection($(this).data('form-id'), $(this).data('section-id')); - }); });