Skip to content
Merged

Dev #2655

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7bc9003
feature/AB#32580-TemplateAttachments
JamesPasta Jul 8, 2026
a3f09b2
feature/AB#32580-TemplateAttachments-FixJsValidationError
JamesPasta Jul 9, 2026
591aee9
AB#33756 restore attachment status proxy method
jacobwillsmith Jul 8, 2026
bbc6623
AB#33756 align AI generation status contract
jacobwillsmith Jul 8, 2026
cb30550
AB#33756 clean up AI route surface
jacobwillsmith Jul 8, 2026
d94c554
AB#33756 fix ai button state flicker
jacobwillsmith Jul 9, 2026
807b5f0
AB#33756 fix Status enum-to-string mapping in AI generation status
jacobwillsmith Jul 9, 2026
bde3c00
AB#33756 remove stray test base churn
jacobwillsmith Jul 9, 2026
b3b8d14
AB#33756 fix AI button polling state
jacobwillsmith Jul 9, 2026
507dac7
AB#33756 restore attachment summary helper
jacobwillsmith Jul 9, 2026
e2324c9
AB#32311 move attachment summary data provider to host app
jacobwillsmith Jul 7, 2026
e4de5af
AB#32311 fix AI attachment summary tests
jacobwillsmith Jul 8, 2026
2d70ae5
AB#33756 clean up AI route surface
jacobwillsmith Jul 8, 2026
b1f6d7e
AB#33756 fix ai button state flicker
jacobwillsmith Jul 9, 2026
9de2585
AB#33756 resolve rebase cleanup issues
jacobwillsmith Jul 9, 2026
2954618
AB#33756 fix attachment summary and failure reason
jacobwillsmith Jul 9, 2026
0086941
AB#33756 add failure reason to ai status dto
jacobwillsmith Jul 9, 2026
b98dda2
AB#33756 remove dead ai poll cleanup
jacobwillsmith Jul 9, 2026
036fa31
AB#33756 centralize ai web requests
jacobwillsmith Jul 9, 2026
44fc79b
AB#33756 split ai generation api helper
jacobwillsmith Jul 9, 2026
e882bd2
AB#33756 fix ai attachment summary provider tests
jacobwillsmith Jul 9, 2026
60e9a0d
AB#33756 remove duplicate ai assessment handlers
jacobwillsmith Jul 9, 2026
90d0ee2
Merge pull request #2636 from bcgov/feature/AB#33756-ai-route-surface…
JamesPasta Jul 10, 2026
bbfd1b8
Merge pull request #2654 from bcgov/feature/AB#32580-TemplateAttachments
JamesPasta Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public virtual async Task<AIGenerationStatusDto> GetStatusAsync(Guid application
FailureReason = request.FailureReason,
IsActive = request.IsActive
},
FailureReason = request?.FailureReason,
IsGenerating = state.IsGenerating,
RetryAfterSeconds = state.RetryAfterSeconds
};
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public interface IGrantApplicationAppService
Task<Guid?> GetAccountCodingIdFromFormIdAsync(Guid formId);
Task<string> DismissAIAnalysisItemAsync(Guid applicationId, string itemId);
Task<string> RestoreAIAnalysisItemAsync(Guid applicationId, string itemId);
Task<AIGenerationStatusDto> GetAIGenerationStatusAsync(Guid applicationId, string operationType);
Task<PagedResultDto<GrantApplicationDto>> GetListAsync(GrantApplicationListInputDto input);
Task<bool> IsApplicantRedStopAsync(Guid applicationId);
string? GetWorkflowDiagram(bool isDirectApproval);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ public static Dictionary<string, string> 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,
Expand All @@ -68,6 +68,8 @@ public static Dictionary<string, string> 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
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -1358,14 +1357,30 @@ public async Task<string> DismissAIAnalysisItemAsync(Guid applicationId, string
return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: true);
}

public async Task<string> RestoreAIAnalysisItemAsync(Guid applicationId, string itemId)
{
return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: false);
}

private async Task<string> UpdateAIAnalysisItemDismissedStateAsync(Guid applicationId, string itemId, bool isDismissed)
{
if (string.IsNullOrWhiteSpace(itemId))
public async Task<string> RestoreAIAnalysisItemAsync(Guid applicationId, string itemId)
{
return await UpdateAIAnalysisItemDismissedStateAsync(applicationId, itemId, isDismissed: false);
}

public async Task<AIGenerationStatusDto> 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<string> UpdateAIAnalysisItemDismissedStateAsync(Guid applicationId, string itemId, bool isDismissed)
{
if (string.IsNullOrWhiteSpace(itemId))
{
throw new UserFriendlyException("AI analysis item id is required.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@
<abp-style src="/libs/formiojs/formio.form.css" />
<abp-style src="/Pages/GrantApplications/Details.css" />
}
@section scripts
{
<abp-script src="/Pages/GrantApplications/ai-generation-button-state.js" />
<abp-script src="/Pages/GrantApplications/ai-rate-limit.js" />
<abp-script src="/Pages/GrantApplications/Details.js" />
@section scripts
{
<abp-script src="/Pages/GrantApplications/ai-generation-api.js" />
<abp-script src="/Pages/GrantApplications/ai-generation-button-state.js" />
<abp-script src="/Pages/GrantApplications/ai-rate-limit.js" />
<abp-script src="/Pages/GrantApplications/Details.js" />
<abp-script src="/libs/formiojs/formio.form.min.js" />
<abp-script src="/Pages/GrantApplications/ai-analysis.js" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
}
});
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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);
Expand All @@ -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 });
Expand Down Expand Up @@ -114,4 +113,5 @@
return { stop };
},
};

})(globalThis);
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -710,7 +704,6 @@ function queueApplicationScoring(triggerButton = null) {
PubSub.publish('refresh_assessment_scores', null);
return;
}

monitorScoring();
})
.fail(function () {
Expand Down Expand Up @@ -745,4 +738,5 @@ $(function () {
$(document).on('click', '[id^="scoresheet-section-discard-"]', function () {
discardChangesScoresSection($(this).data('form-id'), $(this).data('section-id'));
});

});
Loading
Loading