Skip to content
Merged

Dev #2817

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d7eea40
AB#33821 - Added legal disclaimer modal
hasanpour Aug 13, 2026
8788800
AB#33553: Wrong Tenant Error Page
aurelio-aot Aug 13, 2026
19d81b6
AB#33821 - Fixed camelCase file refrence
hasanpour Aug 13, 2026
5584ae6
AB#34091 fix the reporting delete module overlay
AndreGAot Aug 13, 2026
17544ae
AB#33815 - Added backend
hasanpour Aug 13, 2026
5605b37
AB#33815 - Added UI
hasanpour Aug 13, 2026
c98fbbd
AB#33815 - Gating wiring
hasanpour Aug 13, 2026
453938a
AB#33815 - Fixed AI Reporting permission gap
hasanpour Aug 13, 2026
47e916e
AB#33815 - Localized AISettingGroup
hasanpour Aug 13, 2026
a067f14
Merge pull request #2812 from bcgov/bugfix/AB34091-reporting-config-o…
AndreGAot Aug 13, 2026
02080a8
AB#33815 - Fixed menu item visibility for ITAdmin
hasanpour Aug 13, 2026
433f390
[AB#34096] Fix ApplicationFormVersion update functions
plavoie-BC Aug 13, 2026
bd77d48
Merge pull request #2814 from bcgov/bugfix/AB#34096-form-exception
JamesPasta Aug 13, 2026
b63f825
AB#34100 fix program details display logic
AndreGAot Aug 14, 2026
ef913dd
AB#33864 implement AI mapping worksheet suggestion flow
jacobwillsmith Aug 5, 2026
eaadb8f
AB#33864 guide independent mapping and worksheet flow
jacobwillsmith Aug 5, 2026
b7dcd68
AB#33864 consolidate AI review workflow migrations
jacobwillsmith Aug 6, 2026
327c2fc
AB#33864 refine AI mapping worksheet review flow
jacobwillsmith Aug 7, 2026
3b1d12b
AB#33864 remove migration history reconciliation
jacobwillsmith Aug 7, 2026
9b5f24a
AB#33864 normalize mapping assets after rebase
jacobwillsmith Aug 12, 2026
c42b164
AB#33864 add AI scoresheet suggestion review flow
jacobwillsmith Aug 13, 2026
b7549e3
AB#33864 fix worksheet review test fixtures
jacobwillsmith Aug 13, 2026
87af0c1
AB#33864 improve AI suggestion review flow
jacobwillsmith Aug 13, 2026
841945f
AB#33864 correct AI flow reset cleanup
jacobwillsmith Aug 13, 2026
3e83925
AB#33864 clean up AI review warnings
jacobwillsmith Aug 13, 2026
d3c39a2
AB#33864 fix remaining SonarCloud guard
jacobwillsmith Aug 13, 2026
94474b2
AB#33864 clean nullable warnings in AI tests
jacobwillsmith Aug 13, 2026
fd33dd4
AB#33864 fix AI test compile errors
jacobwillsmith Aug 13, 2026
19541b8
AB#33864 fix prompt seeder test mock
jacobwillsmith Aug 13, 2026
f7b2856
AB#33864 stabilize prompt seeder test
jacobwillsmith Aug 13, 2026
6f834db
AB#33864 match prompt repository overloads
jacobwillsmith Aug 13, 2026
5e07a65
AB#33864 address AI review workflow findings
jacobwillsmith Aug 13, 2026
0a18172
AB#33864 stabilize prompt seeder regression test
jacobwillsmith Aug 13, 2026
f694c67
AB#33864 align scoresheet seed assertion
jacobwillsmith Aug 13, 2026
9c8c89b
AB#33864 finalize AI review workflow
jacobwillsmith Aug 14, 2026
f007732
Merge pull request #2815 from bcgov/bugfix/AB#34100-program-details-m…
JamesPasta Aug 14, 2026
20c7fca
Merge pull request #2813 from bcgov/feature/AB#33815-AI-Reporting-Toggle
JamesPasta Aug 14, 2026
a977425
Merge pull request #2811 from bcgov/bugfix/AB#33553-Wrong-Tenant-Message
JamesPasta Aug 14, 2026
ebf9834
bugfix/AB#34067 - Fix Cypress lists
velang01 Aug 14, 2026
715526e
bugfix/AB#34092-ImportUserBroken
JamesPasta Aug 14, 2026
389f884
Merge pull request #2816 from bcgov/bugfix/AB#34092-Import-User-Broke…
JamesPasta Aug 14, 2026
186556c
Merge pull request #2809 from bcgov/feature/AB#33864-ai-mapping-works…
JamesPasta Aug 14, 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
35 changes: 19 additions & 16 deletions applications/Unity.AutoUI/cypress/e2e/lists.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,34 @@ describe('Grant Manager Login and List Navigation', () => {
const appsPage = ApplicationsPageInstance()

function setDashboardIntakeToTestIfAvailable() {
const btnSel = 'button[data-id="dashboardIntakeId"]'
const listboxSel = '#bs-select-1[role="listbox"]'
const searchSel = 'input[type="search"][aria-controls="bs-select-1"]'

cy.get(btnSel, { timeout: 30000 })
// The INTAKES filter is a Select2 (bootstrap-5 theme) multi-select, not
// bootstrap-select — the toggle is the [role="combobox"] wrapping the
// rendered-choices <ul id="select2-dashboardIntakeId-container">.
const renderedSel = '#select2-dashboardIntakeId-container'
const dropdownSel = '.select2-dropdown'
const optionSel = `${dropdownSel} li.select2-results__option`

cy.get(renderedSel, { timeout: 30000 })
.should('be.visible')
.first()
.closest('[role="combobox"]')
.as('intakeCombobox')
.click({ force: true })

cy.get(listboxSel, { timeout: 30000 }).should('be.visible')
cy.get(dropdownSel, { timeout: 30000 }).should('be.visible')

cy.get(searchSel, { timeout: 30000 })
cy.get(renderedSel)
.parent()
.find('textarea.select2-search__field', { timeout: 30000 })
.should('be.visible')
.clear()
.type('Test')

cy.contains(`${listboxSel} a.dropdown-item[role="option"] span.text`, /^Test$/, { timeout: 30000 })
.closest('a.dropdown-item')
cy.contains(optionSel, /^Test$/, { timeout: 30000 })
.then(($opt) => {
const selected =
$opt.attr('aria-selected') === 'true' ||
$opt.hasClass('selected')
const selected = $opt.attr('aria-selected') === 'true'

if (!selected) {
cy.wrap($opt).scrollIntoView().click({ force: true })
cy.wrap($opt).click({ force: true })
}
})

Expand All @@ -43,8 +46,8 @@ describe('Grant Manager Login and List Navigation', () => {
expect(texts).to.include('Test')
})

cy.get(btnSel).first().click({ force: true })
cy.get(btnSel).first().should('have.attr', 'aria-expanded', 'false')
cy.get('@intakeCombobox').click({ force: true })
cy.get('@intakeCombobox').should('have.attr', 'aria-expanded', 'false')
}

it('Verify Login', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ Use these existing operations as the canonical references:
6. Expose a generate surface and UI only when users need one.
7. Add focused catalog, runtime, executor, and persistence tests.

## Staged form mapping

`FormMapping` and `FormWorksheet` remain independent operations. The Mapping
tab can guide an administrator through them as a staged form-configuration flow:

1. Generate mapping suggestions using the form fields, core fields, assigned
custom fields, and current saved mapping.
2. Persist suggestions for review; accept them individually so existing
non-empty mappings always win.
3. Generate and review `FormWorksheet` suggestions, either after mapping review
or directly from the idle state.
4. Publish and assign created worksheet drafts through the normal worksheet
configuration UI.
5. Invoke the same `FormMapping` operation again after publication and
assignment.

Keep mapping review state scoped to the form version. Do not auto-link unpublished
AI worksheet drafts to a UI anchor just to make their fields visible to mapping.

## Rules
- Keep prompt content and operation/model configuration in the database.
- Reuse the shared generation pipeline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Generate a recommended worksheet definition for a form version.

## Output Shape
- A `fields` collection containing the suggested additional worksheet fields.
- Include all applicable additional fields in the collection; do not limit the response to one suggestion.
- Each suggestion supplies the field key, label, and supported custom-field type.
- The executor builds the worksheet and its `Suggested Fields` section from the validated suggestions.
- Keep the result valid JSON and include only fields that the form needs beyond core Unity fields.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
- `FormWorksheet` - worksheet generation
- `FormScoresheet` - scoresheet generation

`FormMapping` and `FormWorksheet` are independent operations with complete
context inputs. The Mapping tab can guide them through mapping review, worksheet
review, publish and assignment, and another mapping run. Review suggestions are
persisted until accepted or discarded. Initial-review suggestions preserve existing
non-empty mappings; accepted final-review suggestions may replace conflicting mappings.

## Versions
- Built-in prompt rows are defined and seeded by `AIPromptDataSeeder`.
- Families may have `v0`, `v1`, and `v2` rows; a new operation only needs the versions it supports.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public override void Define(IPermissionDefinitionContext context)
"Unity.AI.ApplicationAnalysis",
"Unity.AI.FormMapping",
"Unity.AI.FormWorksheet",
"Unity.AI.FormScoresheet"));
"Unity.AI.FormScoresheet",
"Unity.AIReporting"));
}

private static LocalizableString L(string name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public class FormMappingResponse
{
public string Mapping { get; set; } = string.Empty;

public string? FailureReason { get; set; }

[JsonPropertyName("coreFieldMatches")]
public List<FormMappingMatchResponse> CoreFieldMatches { get; set; } = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public class FormScoresheetResponse
{
public string Scoresheet { get; set; } = string.Empty;

public string? FailureReason { get; set; }

[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public class AITenantConfigurationDto
{
public bool AutomaticGenerationEnabled { get; set; }
public bool ManualGenerationEnabled { get; set; }
public bool ReportingEnabled { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public class UpdateAITenantConfigurationDto
{
public bool AutomaticGenerationEnabled { get; set; }
public bool ManualGenerationEnabled { get; set; }
public bool ReportingEnabled { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class AIOperationDataSeeder(
new(AIPromptTypes.ApplicationScoring, 8000),
new(AIPromptTypes.FormMapping, 2000),
new(AIPromptTypes.FormWorksheet, 4000),
new(AIPromptTypes.FormScoresheet, 4000)
new(AIPromptTypes.FormScoresheet, 8000)
];

public async Task SeedAsync(DataSeedContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,8 @@ Return only valid JSON.
}

Rules:
- Return one field-suggestion JSON object only.
- Return one JSON object containing all applicable field suggestions. Review the full form schema and every unmapped CHEFS field before responding; include every additional custom field genuinely needed, not just the first match.
- Return an empty fields array when no additional custom fields are needed.
- chefsFields contains the available CHEFS source fields.
- unityCoreFields contains existing Unity core fields. Do not create a custom field when one of these already fits.
- existingMapping contains the current saved Unity-to-CHEFS mappings. Do not duplicate those mappings with a custom field.
Expand Down Expand Up @@ -932,6 +933,8 @@ Return only valid JSON.

Rules:
- Return one scoresheet definition JSON object only.
- Title and Name must be non-empty strings; Sections must contain at least one section and every section must contain at least one field.
- Every field Name and Label must be non-empty, Order and Type must be non-negative integers, and Definition must be a valid JSON object encoded as a string.
- The context contains CHEFS form fields, allowed Unity Flex question types, and a scoresheet template.
- Fill out the scoresheet template to generate the rubric assessors use to score submitted applications.
- Use CHEFS form fields as evidence for assessment criteria, but do not create one question per form field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public enum AIFailureCategory
public sealed record AIOperationResult(
AIOperationOutcome Outcome,
AIProviderResult Response,
AIFailureCategory FailureCategory = AIFailureCategory.None)
AIFailureCategory FailureCategory = AIFailureCategory.None,
string? FailureReason = null)
{
public string Content => Response.Content;

Expand All @@ -41,8 +42,8 @@ public static AIOperationResult ProviderUnavailable(AIProviderResult? response =
public static AIOperationResult InvalidOutput(AIProviderResult? response = null) =>
new(AIOperationOutcome.InvalidOutput, response ?? AIProviderResult.Empty, AIFailureCategory.InvalidOutput);

public AIOperationResult WithOutcome(AIOperationOutcome outcome, AIFailureCategory? failureCategory = null) =>
new(outcome, Response, failureCategory ?? ResolveFailureCategory(outcome));
public AIOperationResult WithOutcome(AIOperationOutcome outcome, AIFailureCategory? failureCategory = null, string? failureReason = null) =>
new(outcome, Response, failureCategory ?? ResolveFailureCategory(outcome), failureReason ?? FailureReason);

private static AIFailureCategory ResolveFailureCategory(AIOperationOutcome outcome)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ private static AIResponseValidationResult ValidateSections(JsonElement root, str
return sectionNameResult;
}

if (!sectionNames.Add(section.GetProperty("Name").GetString()!))
if (!TryGetProperty(section, "Name", out var sectionName)
|| !sectionNames.Add(sectionName.GetString()!))
{
return AIResponseValidationResult.Invalid($"{responseName} response contains duplicate section names.");
}
Expand Down Expand Up @@ -235,7 +236,8 @@ private static AIResponseValidationResult ValidateSections(JsonElement root, str
return result;
}

if (!fieldNames.Add(field.GetProperty("Name").GetString()!))
if (!TryGetProperty(field, "Name", out var fieldName)
|| !fieldNames.Add(fieldName.GetString()!))
{
return AIResponseValidationResult.Invalid($"{responseName} response contains duplicate field names.");
}
Expand Down Expand Up @@ -355,6 +357,18 @@ private static bool TryGetProperty(JsonElement element, string propertyName, out
return true;
}

if (element.ValueKind == JsonValueKind.Object)
{
foreach (var candidate in element.EnumerateObject())
{
if (string.Equals(candidate.Name, propertyName, StringComparison.OrdinalIgnoreCase))
{
property = candidate.Value;
return true;
}
}
}

property = default;
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,8 @@ public async Task<FormScoresheetResponse> GenerateFormScoresheetAsync(FormScores

return new FormScoresheetResponse
{
Scoresheet = result.Outcome == AIOperationOutcome.Success
? AIResponseJson.CleanJsonResponse(result.Content)
: "{}"
Scoresheet = AIResponseJson.CleanJsonResponse(result.Content),
FailureReason = result.FailureReason
};
}
catch (OperationCanceledException)
Expand Down Expand Up @@ -406,7 +405,10 @@ private async Task<FormMappingResponse> GenerateFormMappingCoreAsync(FormMapping

if (result.Outcome != AIOperationOutcome.Success)
{
return new FormMappingResponse();
return new FormMappingResponse
{
FailureReason = result.FailureReason ?? $"Mapping generation failed with outcome {result.Outcome}."
};
}

return new FormMappingResponse
Expand All @@ -421,7 +423,7 @@ private async Task<FormMappingResponse> GenerateFormMappingCoreAsync(FormMapping
catch (Exception ex)
{
_logger.LogError(ex, "Mapping suggestion generation failed.");
return new FormMappingResponse();
return new FormMappingResponse { FailureReason = ex.Message };
}
}

Expand Down Expand Up @@ -449,7 +451,10 @@ private async Task<AIOperationResult> GenerateWithRetryAsync(
return lastResult;
}

lastResult = lastResult.WithOutcome(AIOperationOutcome.InvalidOutput, validationResult.FailureCategory);
lastResult = lastResult.WithOutcome(
AIOperationOutcome.InvalidOutput,
validationResult.FailureCategory,
validationResult.Reason);

_logger.LogWarning(
"AI {OperationName} attempt {Attempt}/{MaxAttempts} returned invalid response shape ({FailureCategory}): {Reason}; will retry if attempts remain",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public virtual async Task<AITenantConfigurationDto> GetTenantConfigurationAsync(
AutomaticGenerationEnabled = await _settingProvider.GetAsync<bool>(
AISettings.AutomaticGenerationEnabled, defaultValue: false),
ManualGenerationEnabled = await _settingProvider.GetAsync<bool>(
AISettings.ManualGenerationEnabled, defaultValue: false)
AISettings.ManualGenerationEnabled, defaultValue: false),
ReportingEnabled = await _settingProvider.GetAsync<bool>(
AISettings.ReportingEnabled, defaultValue: false)
};
}

Expand All @@ -46,5 +48,11 @@ await _settingManager.SetAsync(
input.ManualGenerationEnabled.ToString().ToLowerInvariant(),
TenantSettingValueProvider.ProviderName,
_currentTenant.Id?.ToString());

await _settingManager.SetAsync(
AISettings.ReportingEnabled,
input.ReportingEnabled.ToString().ToLowerInvariant(),
TenantSettingValueProvider.ProviderName,
_currentTenant.Id?.ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ public override void Define(ISettingDefinitionContext context)
isEncrypted: false)
.WithProviders(TenantSettingValueProvider.ProviderName)
);

context.Add(
new SettingDefinition(
AISettings.ReportingEnabled,
"false",
L("Setting:AI.ReportingEnabled"),
isVisibleToClients: false,
isInherited: false,
isEncrypted: false)
.WithProviders(TenantSettingValueProvider.ProviderName)
);
}

private static LocalizableString L(string name)
Expand Down
Loading
Loading