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..3a832df03 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs @@ -0,0 +1,14 @@ +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; + + 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 54362b276..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 }; @@ -117,9 +118,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/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.'); } 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; } } 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/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.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AttachmentSummaryDataProvider.cs index e0f416dff..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 @@ -1,14 +1,14 @@ -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; +using Volo.Abp; namespace Unity.GrantManager.GrantApplications.Automation; @@ -23,16 +23,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) 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..52adccaaf 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,30 @@ 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, + FailureReason = request?.FailureReason + }; + } + + private async Task UpdateAIAnalysisItemDismissedStateAsync(Guid applicationId, string itemId, bool isDismissed) + { + if (string.IsNullOrWhiteSpace(itemId)) { throw new UserFriendlyException("AI analysis item id is required."); } 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-analysis.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-analysis.js index b68bc63b9..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,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' - }) + globalThis.AIGenerationApi.queueApplicationAnalysis(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: () => globalThis.AIGenerationApi.getStatus(applicationId, 'application-analysis'), onComplete: loadAIAnalysis, onFailed: (request) => { loadAIAnalysis(); @@ -574,19 +568,13 @@ $(function() { return; } - abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(applicationId)}&operationType=application-analysis`, - type: 'GET' - }) - .done(function(generationStatus) { - const request = generationStatus?.generationRequest; - if (request?.isActive !== true) { - return; - } - - const existingHtml = $regenerateButton.html(); - globalThis.AIGenerationButtonState?.setGenerating($regenerateButton); - monitorAIAnalysisGeneration(applicationId, $regenerateButton, existingHtml); - }); + globalThis.AIGenerationApi.getStatus(applicationId, 'application-analysis').done(function(generationStatus) { + if (generationStatus?.generationRequest?.isActive !== true) { + return; + } + 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-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 c11b0c1ee..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 @@ -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 } @@ -73,10 +72,10 @@ stop(); restoreButton(options.$button, options.originalHtml); applyRateLimitState(generationStatus, { pollWhenGenerating: true }); + options.onPollFailed?.(new Error(generationStatus?.failureReason || 'AI generation failed.')); options.onFailed?.(request); return; } - if (!request) { stop(); restoreButton(options.$button, options.originalHtml); @@ -85,7 +84,7 @@ return; } - if (request.isActive === false || status === 'Completed') { + if (request.isActive !== true) { stop(); restoreButtonForCooldownCheck(options.$button, options.originalHtml); applyRateLimitState(generationStatus, { pollWhenGenerating: true }); @@ -114,4 +113,5 @@ return { stop }; }, }; + })(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 369eb3dda..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 @@ -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(); @@ -710,7 +704,6 @@ function queueApplicationScoring(triggerButton = null) { PubSub.publish('refresh_assessment_scores', null); return; } - monitorScoring(); }) .fail(function () { @@ -745,4 +738,5 @@ $(function () { $(document).on('click', '[id^="scoresheet-section-discard-"]', function () { discardChangesScoresSection($(this).data('form-id'), $(this).data('section-id')); }); + }); 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..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,49 +202,34 @@ $(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(); - }, - }); + }); }); } 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: () => 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.'); + 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/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 @@ - + 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..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,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' - }) + globalThis.AIGenerationApi.queueApplicationScoring(pageApplicationId) .done(function (generationStatus) { const request = generationStatus?.generationRequest; const status = String(request?.status ?? '').trim(); @@ -485,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) { @@ -520,29 +515,21 @@ function resumeActiveReviewListAiButton(reviewListTable) { } const $button = $(button.node()); - abp.ajax({ - url: `/api/app/ai/generation/status?applicationId=${encodeURIComponent(pageApplicationId)}&operationType=application-scoring`, - type: 'GET' - }) - .done(function(generationStatus) { - const request = generationStatus?.generationRequest; - if (request?.isActive !== true) { - return; - } + globalThis.AIGenerationApi.getStatus(pageApplicationId, 'application-scoring').done(function(generationStatus) { + if (generationStatus?.generationRequest?.isActive !== true) { + return; + } - globalThis.AIGenerationButtonState?.setGenerating($button); - pollReviewListAiButton($button); - }); + globalThis.AIGenerationButtonState?.setGenerating($button); + pollReviewListAiButton($button); + }); } 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: () => globalThis.AIGenerationApi.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/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index dad6869bc..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 @@ -2,13 +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; using Unity.AI; @@ -17,6 +14,7 @@ using Unity.AI.Operations; using Unity.AI.Requests; using Unity.AI.Responses; +using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; using Volo.Abp; using Volo.Abp.Uow; @@ -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() { 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..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 @@ -5,17 +5,18 @@ using System.Collections.Generic; using System.Threading.Tasks; using Unity.AI.Automation; -using Unity.AI.Generation; +using Unity.AI.Domain; using Unity.AI.Localization; +using Unity.AI.Generation; using Unity.AI.Operations; using Unity.AI.RateLimit; 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); } - } - 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);