diff --git a/dbschema/language.gel b/dbschema/language.gel index 909aa72b5d..5bd86902a0 100644 --- a/dbschema/language.gel +++ b/dbschema/language.gel @@ -57,6 +57,14 @@ module default { constraint regexp(r'^[0-9]{5}$'); } + property usesAIAssistance := exists ( + select .engagements + filter .usingAIAssistedTranslation not in { + Engagement::AIAssistedTranslation.None, + Engagement::AIAssistedTranslation.Unknown + } + ); + property population := .populationOverride ?? .ethnologue.population; populationOverride: population; diff --git a/dbschema/migrations/00020-m1cpuxn.edgeql b/dbschema/migrations/00020-m1cpuxn.edgeql new file mode 100644 index 0000000000..20f1f7db24 --- /dev/null +++ b/dbschema/migrations/00020-m1cpuxn.edgeql @@ -0,0 +1,11 @@ +CREATE MIGRATION m1cpuxnjoiyehcwsjdlul7vt4s4audncdb3immaclqupzrr27ud44a + ONTO m1lyywc5pcxyfanxv2acytadtefwklgvbscl4dpm5fnfsjivxvixea +{ + ALTER TYPE default::Language { + CREATE PROPERTY usesAIAssistance := (EXISTS ((SELECT + .engagements + FILTER + (.usingAIAssistedTranslation NOT IN {Engagement::AIAssistedTranslation.None, Engagement::AIAssistedTranslation.Unknown}) + ))); + }; +}; diff --git a/src/components/language/dto/language.dto.ts b/src/components/language/dto/language.dto.ts index 4cda300888..0b3a053201 100644 --- a/src/components/language/dto/language.dto.ts +++ b/src/components/language/dto/language.dto.ts @@ -184,6 +184,17 @@ export class Language extends Interfaces { }) readonly presetInventory: SecuredBoolean; + @Calculated() + @Field({ + description: stripIndent` + Whether any engagement for this language is using AI-assisted translation. + + This is true if any engagement's "usingAIAssistedTranslation" property is not "None" or "Unknown". + Used to track and filter languages that have AI assistance in their translation process. + `, + }) + readonly usesAIAssistance: SecuredBoolean; + // Not returned, only used to cache the sensitivity for determining permissions readonly effectiveSensitivity: Sensitivity; } diff --git a/src/components/language/dto/list-language.dto.ts b/src/components/language/dto/list-language.dto.ts index 5efca5cc86..dea58a5076 100644 --- a/src/components/language/dto/list-language.dto.ts +++ b/src/components/language/dto/list-language.dto.ts @@ -47,6 +47,12 @@ export abstract class LanguageFilters { }) readonly presetInventory?: boolean; + @OptionalField({ + description: + 'Only languages that have an AI-assisted translation engagement', + }) + readonly usesAIAssistance?: boolean; + @OptionalField({ description: 'Only languages that are pinned/unpinned by the requesting user', diff --git a/src/components/language/language.gel.repository.ts b/src/components/language/language.gel.repository.ts index feca00f9de..4a84aa1766 100644 --- a/src/components/language/language.gel.repository.ts +++ b/src/components/language/language.gel.repository.ts @@ -15,6 +15,7 @@ export class LanguageGelRepository sensitivity: lang.ownSensitivity, effectiveSensitivity: lang.sensitivity, presetInventory: e.bool(false), // Not implemented going forward + usesAIAssistance: lang.usesAIAssistance, }), omit: ['create'], }) diff --git a/src/components/language/language.repository.ts b/src/components/language/language.repository.ts index 8f9a81603f..2fb1c6bf72 100644 --- a/src/components/language/language.repository.ts +++ b/src/components/language/language.repository.ts @@ -209,6 +209,7 @@ export class LanguageRepository extends DtoRepository< ]) .apply(matchProps({ nodeName: 'eth', outputVar: 'ethProps' })) .apply(isPresetInventory) + .apply(usingAIAssistance) .optionalMatch([ node('node'), relation('in', '', 'language', ACTIVE), @@ -222,6 +223,7 @@ export class LanguageRepository extends DtoRepository< ethnologue: 'ethProps', pinned, presetInventory: 'presetInventory', + usesAIAssistance: 'usesAIAssistance', firstScriptureEngagement: 'firstScriptureEngagement { .id }', scope: 'scopedRoles', changeset: 'changeset.id', @@ -339,6 +341,10 @@ export const languageFilters = filter.define(() => LanguageFilters, { const condition = equals('true', true); return { presetInventory: value ? condition : not(condition) }; }, + usesAIAssistance: ({ value, query }) => { + query.apply(usingAIAssistance).with(['node', 'usesAIAssistance']); + return { usesAIAssistance: value }; + }, }); const ethnologueFilters = filter.define(() => EthnologueLanguageFilters, { @@ -374,6 +380,25 @@ const isPresetInventory = (query: Query) => ), ); +const usingAIAssistance = (query: Query) => + query.subQuery('node', (sub) => + sub + .optionalMatch([ + node('node'), + relation('in', '', 'language', ACTIVE), + node('eng', 'LanguageEngagement'), + ]) + .optionalMatch([ + node('eng'), + relation('out', '', 'usingAIAssistedTranslation', ACTIVE), + node('prop', 'Property'), + ]) + .with([ + `any(val in collect(prop.value) WHERE val IS NOT NULL AND val <> 'None' AND val <> 'Unknown') as usesAIAssistance`, + ]) + .return(['usesAIAssistance']), + ); + export const languageSorters = defineSorters(Language, { // eslint-disable-next-line @typescript-eslint/naming-convention 'ethnologue.*': (query, input) => @@ -405,6 +430,11 @@ export const languageSorters = defineSorters(Language, { .return<{ sortValue: unknown }>( coalesce('override.value', 'canonical.value').as('sortValue'), ), + usesAIAssistance: (query) => + query + .apply(usingAIAssistance) + .with(['node', 'usesAIAssistance as sortValue']) + .return<{ sortValue: unknown }>('sortValue'), }); const ethnologueSorters = defineSorters(EthnologueLanguage, {});