From d1451e56921070415de751a75f9274f70d7c3fa0 Mon Sep 17 00:00:00 2001 From: Julie Huang <julhuang@gitlab.com> Date: Thu, 3 Oct 2024 13:30:35 +0000 Subject: [PATCH] Invoke aiFeatureSettings query - Invoke query - Update tests - Remove data stub --- .../ai/feature_settings/components/app.vue | 59 ++++++++----- .../components/feature_settings_table.vue | 11 +-- .../components/model_select_dropdown.vue | 69 ++++++++------- .../admin/ai/feature_settings/data_stub.js | 34 ------- ...pdate_ai_feature_setting.mutation.graphql} | 0 .../get_ai_feature_settings.query.graphql | 22 +++++ .../admin/ai/feature_settings_controller.rb | 4 - .../admin/ai/feature_settings/index.html.haml | 2 +- .../admin/ai/feature_settings/app_spec.js | 88 ++++++++++++++++++- .../feature_settings_table_spec.js | 5 +- .../admin/ai/feature_settings/mock_data.js | 25 ++++-- .../model_select_dropdown_spec.js | 5 +- .../ai/feature_settings_controller_spec.rb | 14 --- locale/gitlab.pot | 9 +- 14 files changed, 212 insertions(+), 135 deletions(-) delete mode 100644 ee/app/assets/javascripts/pages/admin/ai/feature_settings/data_stub.js rename ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/mutations/{update_ai_feature_setting.graphql => update_ai_feature_setting.mutation.graphql} (100%) create mode 100644 ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/queries/get_ai_feature_settings.query.graphql diff --git a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/app.vue b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/app.vue index ff0d99246bbac..268fcbbc0be4c 100644 --- a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/app.vue +++ b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/app.vue @@ -1,52 +1,69 @@ <script> -import stubbedAiFeatureSettings from '../data_stub'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { createAlert } from '~/alert'; +import getAiFeatureSettingsQuery from '../graphql/queries/get_ai_feature_settings.query.graphql'; import AiFeatureSettingsTable from './feature_settings_table.vue'; export default { name: 'FeatureSettingsApp', components: { + GlSkeletonLoader, AiFeatureSettingsTable, }, props: { - models: { - type: Array, - required: true, - }, newSelfHostedModelPath: { type: String, required: true, }, - // Return stubbed data for now - featureSettings: { - type: Array, - required: false, - default: () => { - return stubbedAiFeatureSettings; + }, + i18n: { + title: s__('AdminAIPoweredFeatures|AI-powered features'), + description: s__( + 'AdminAIPoweredFeatures|Features that can be enabled, disabled, or linked to a cloud-based or self-hosted model.', + ), + errorMessage: s__( + 'AdminAIPoweredFeatures|An error occurred while loading the AI feature settings. Please try again.', + ), + }, + data() { + return { + aiFeatureSettings: [], + }; + }, + apollo: { + aiFeatureSettings: { + query: getAiFeatureSettingsQuery, + update(data) { + return data?.aiFeatureSettings?.nodes || []; + }, + error(error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); }, }, }, + computed: { + isLoading() { + return this.$apollo?.queries?.aiFeatureSettings?.loading; + }, + }, }; </script> <template> <div> <section> <h1 class="page-title gl-text-size-h-display"> - {{ s__('AdminAIPoweredFeatures|AI-powered features') }} + {{ $options.i18n.title }} </h1> <div class="gl-items-top gl-flex gl-justify-between"> - <p> - {{ - s__( - 'AdminAIPoweredFeatures|Features that can be enabled, disabled, or linked to a cloud-based or self-hosted model.', - ) - }} - </p> + <p>{{ $options.i18n.description }}</p> </div> </section> + <gl-skeleton-loader v-if="isLoading" /> <ai-feature-settings-table - :feature-settings="featureSettings" + v-else + :ai-feature-settings="aiFeatureSettings" :new-self-hosted-model-path="newSelfHostedModelPath" - :models="models" /> </div> </template> diff --git a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/feature_settings_table.vue b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/feature_settings_table.vue index bd53cea0cb857..5a3bbdab750fb 100644 --- a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/feature_settings_table.vue +++ b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/feature_settings_table.vue @@ -10,7 +10,7 @@ export default { ModelSelectDropdown, }, props: { - featureSettings: { + aiFeatureSettings: { type: Array, required: true, }, @@ -18,10 +18,6 @@ export default { type: String, required: true, }, - models: { - type: Array, - required: true, - }, }, fields: [ { @@ -48,7 +44,7 @@ export default { <template> <gl-table-lite :fields="$options.fields" - :items="featureSettings" + :items="aiFeatureSettings" stacked="md" :hover="true" :selectable="false" @@ -61,8 +57,7 @@ export default { </template> <template #cell(model_name)="{ item }"> <model-select-dropdown - :feature-setting="item" - :models="models" + :ai-feature-setting="item" :new-self-hosted-model-path="newSelfHostedModelPath" /> </template> diff --git a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/model_select_dropdown.vue b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/model_select_dropdown.vue index 9c71fe4779b20..959eea0e04f7b 100644 --- a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/model_select_dropdown.vue +++ b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/components/model_select_dropdown.vue @@ -2,13 +2,12 @@ import { GlCollapsibleListbox, GlButton } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { createAlert } from '~/alert'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; -import updateAiFeatureSetting from '../graphql/mutations/update_ai_feature_setting.graphql'; +import updateAiFeatureSetting from '../graphql/mutations/update_ai_feature_setting.mutation.graphql'; const PROVIDERS = { - DISABLED: 'DISABLED', - VENDORED: 'VENDORED', - SELF_HOSTED: 'SELF_HOSTED', + DISABLED: 'disabled', + VENDORED: 'vendored', + SELF_HOSTED: 'self_hosted', }; const FEATURE_DISABLED = 'DISABLED'; @@ -20,14 +19,10 @@ export default { GlButton, }, props: { - featureSetting: { + aiFeatureSetting: { type: Object, required: true, }, - models: { - type: Array, - required: true, - }, newSelfHostedModelPath: { type: String, required: true, @@ -40,17 +35,21 @@ export default { successMessage: s__('AdminSelfHostedModels|Successfully updated %{mainFeature} / %{title}'), }, data() { + const { provider, selfHostedModel, validModels } = this.aiFeatureSetting; + + const selectedOption = provider === PROVIDERS.DISABLED ? FEATURE_DISABLED : selfHostedModel?.id; + return { - feature: this.featureSetting.feature, - provider: this.featureSetting.provider, - selfHostedModelId: this.featureSetting.selfHostedModelId, - selectedOption: null, + provider, + selfHostedModelId: selfHostedModel?.id, + compatibleModels: validModels?.nodes, + selectedOption, isSaving: false, }; }, computed: { dropdownItems() { - const modelOptions = this.models.map((model) => ({ + const modelOptions = this.compatibleModels.map((model) => ({ value: model.id, text: `${model.name} (${model.model})`, })); @@ -64,13 +63,13 @@ export default { return [...modelOptions, disableOption]; }, selectedModel() { - return this.models.find((m) => m.id === this.selfHostedModelId); + return this.compatibleModels.find((m) => m.id === this.selfHostedModelId); }, dropdownToggleText() { - if (!this.selectedOption) { + if (this.provider === PROVIDERS.VENDORED) { return s__('AdminAIPoweredFeatures|Select a self-hosted model'); } - if (this.selectedOption === FEATURE_DISABLED) { + if (this.provider === PROVIDERS.DISABLED) { return s__('AdminAIPoweredFeatures|Disabled'); } @@ -82,18 +81,19 @@ export default { this.isSaving = true; try { - const provider = option === FEATURE_DISABLED ? PROVIDERS.DISABLED : PROVIDERS.SELF_HOSTED; + const selectedOption = { + option, + provider: option === FEATURE_DISABLED ? PROVIDERS.DISABLED : PROVIDERS.SELF_HOSTED, + selfHostedModelId: option === FEATURE_DISABLED ? null : option, + }; const { data } = await this.$apollo.mutate({ mutation: updateAiFeatureSetting, variables: { input: { - feature: this.feature.toUpperCase(), - provider, - selfHostedModelId: - option === FEATURE_DISABLED - ? null - : convertToGraphQLId('Ai::SelfHostedModel', option), + feature: this.aiFeatureSetting.feature.toUpperCase(), + provider: selectedOption.provider.toUpperCase(), + selfHostedModelId: selectedOption.selfHostedModelId, }, }, }); @@ -105,11 +105,9 @@ export default { throw new Error(errors[0]); } - this.selectedOption = option; - this.provider = provider; - this.selfHostedModelId = option === FEATURE_DISABLED ? null : option; + this.updateSelection(selectedOption); this.isSaving = false; - this.$toast.show(this.successMessage(this.featureSetting)); + this.$toast.show(this.successMessage(this.aiFeatureSetting)); } } catch (error) { createAlert({ @@ -120,13 +118,18 @@ export default { this.isSaving = false; } }, + updateSelection(selectedOption) { + this.selectedOption = selectedOption.option; + this.provider = selectedOption.provider; + this.selfHostedModelId = selectedOption.selfHostedModelId; + }, errorMessage(error) { return error.message || this.$options.i18n.defaultErrorMessage; }, - successMessage(featureSetting) { + successMessage(aiFeatureSetting) { return sprintf(this.$options.i18n.successMessage, { - mainFeature: featureSetting.mainFeature, - title: featureSetting.title, + mainFeature: aiFeatureSetting.mainFeature, + title: aiFeatureSetting.title, }); }, }, @@ -138,7 +141,7 @@ export default { :selected="selectedOption" :items="dropdownItems" :toggle-text="dropdownToggleText" - :header-text="s__('AdminAIPoweredFeatures|Self-hosted models')" + :header-text="s__('AdminAIPoweredFeatures|Compatible models')" :loading="isSaving" category="primary" block diff --git a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/data_stub.js b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/data_stub.js deleted file mode 100644 index bfc0150afa9f5..0000000000000 --- a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/data_stub.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Temporary data stub for feature settings table - * - * TODO: Remove when feature settings data is ready. - * See https://gitlab.com/gitlab-org/gitlab/-/issues/473005#note_2076997154 - */ - -/* eslint-disable @gitlab/require-i18n-strings */ -const stubbedAiFeatureSettings = [ - { - feature: 'code_generations', - title: 'Code Generation', - mainFeature: 'Code Suggestions', - provider: 'self_hosted', - selfHostedModel: null, - }, - { - feature: 'code_completions', - title: 'Code Completion', - mainFeature: 'Code Suggestions', - provider: 'vendored', - selfHostedModel: null, - }, - { - feature: 'duo_chat', - title: 'Duo Chat', - mainFeature: 'Duo Chat', - provider: 'self_hosted', - selfHostedModel: null, - }, -]; -/* eslint-enable @gitlab/require-i18n-strings */ - -export default stubbedAiFeatureSettings; diff --git a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.graphql b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.mutation.graphql similarity index 100% rename from ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.graphql rename to ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.mutation.graphql diff --git a/ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/queries/get_ai_feature_settings.query.graphql b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/queries/get_ai_feature_settings.query.graphql new file mode 100644 index 0000000000000..d74d3c62be3d7 --- /dev/null +++ b/ee/app/assets/javascripts/pages/admin/ai/feature_settings/graphql/queries/get_ai_feature_settings.query.graphql @@ -0,0 +1,22 @@ +query getAiFeatureSetting { + aiFeatureSettings { + nodes { + feature + title + mainFeature + provider + selfHostedModel { + id + name + model + } + validModels { + nodes { + id + name + model + } + } + } + } +} diff --git a/ee/app/controllers/admin/ai/feature_settings_controller.rb b/ee/app/controllers/admin/ai/feature_settings_controller.rb index 535dd9b68053a..76be4818c8e9d 100644 --- a/ee/app/controllers/admin/ai/feature_settings_controller.rb +++ b/ee/app/controllers/admin/ai/feature_settings_controller.rb @@ -12,10 +12,6 @@ class FeatureSettingsController < Admin::ApplicationController def index @feature_settings = ::Ai::FeatureSettings::FeatureSettingFinder.new.execute - - return unless Feature.enabled?(:custom_models_feature_settings_vue_app, current_user) - - @self_hosted_models = ::Ai::SelfHostedModel.all end # rubocop:disable CodeReuse/ActiveRecord -- Using find_or_initialize_by is reasonable diff --git a/ee/app/views/admin/ai/feature_settings/index.html.haml b/ee/app/views/admin/ai/feature_settings/index.html.haml index 45398b18e1d9c..73488f924c781 100644 --- a/ee/app/views/admin/ai/feature_settings/index.html.haml +++ b/ee/app/views/admin/ai/feature_settings/index.html.haml @@ -2,7 +2,7 @@ - add_page_specific_style 'page_bundles/labels' - if Feature.enabled?(:custom_models_feature_settings_vue_app, current_user) - #js-ai-powered-features{ data: { view_model: { models: @self_hosted_models, newSelfHostedModelPath: new_admin_ai_self_hosted_model_path }.to_json } } + #js-ai-powered-features{ data: { view_model: { newSelfHostedModelPath: new_admin_ai_self_hosted_model_path }.to_json } } - else = render ::Layouts::CrudComponent.new(s_('AdminAIPoweredFeatures|AI-powered features'), options: { class: 'gl-mt-5' }) do |c| diff --git a/ee/spec/frontend/pages/admin/ai/feature_settings/app_spec.js b/ee/spec/frontend/pages/admin/ai/feature_settings/app_spec.js index 8a7d7852236c6..e39b024ddb8dd 100644 --- a/ee/spec/frontend/pages/admin/ai/feature_settings/app_spec.js +++ b/ee/spec/frontend/pages/admin/ai/feature_settings/app_spec.js @@ -1,17 +1,52 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlSkeletonLoader } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; import FeatureSettingsApp from 'ee/pages/admin/ai/feature_settings/components/app.vue'; +import getAiFeatureSettingsQuery from 'ee/pages/admin/ai/feature_settings/graphql/queries/get_ai_feature_settings.query.graphql'; +import AiFeatureSettingsTable from 'ee/pages/admin/ai/feature_settings/components/feature_settings_table.vue'; +import { createAlert } from '~/alert'; +import { mockAiFeatureSettings } from './mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/alert'); describe('FeatureSettingsApp', () => { let wrapper; - const createComponent = () => { - wrapper = shallowMount(FeatureSettingsApp); + const getAiFeatureSettingsSuccessHandler = jest.fn().mockResolvedValue({ + data: { + aiFeatureSettings: { + nodes: mockAiFeatureSettings, + errors: [], + }, + }, + }); + + const createComponent = ({ + apolloHandlers = [[getAiFeatureSettingsQuery, getAiFeatureSettingsSuccessHandler]], + } = {}) => { + const mockApollo = createMockApollo([...apolloHandlers]); + const newSelfHostedModelPath = '/admin/ai/self_hosted_models/new'; + + wrapper = shallowMount(FeatureSettingsApp, { + apolloProvider: mockApollo, + propsData: { + newSelfHostedModelPath, + }, + }); }; beforeEach(() => { createComponent(); }); + const findAiFeatureSettingsTable = () => wrapper.findComponent(AiFeatureSettingsTable); + const findLoader = () => wrapper.findComponent(GlSkeletonLoader); + it('has a title', () => { const title = wrapper.find('h1'); @@ -23,4 +58,53 @@ describe('FeatureSettingsApp', () => { 'Features that can be enabled, disabled, or linked to a cloud-based or self-hosted model.', ); }); + + describe('when AI feature settings are loading', () => { + it('renders skeleton loader', () => { + expect(findLoader().exists()).toBe(true); + }); + }); + + describe('when the API request is successful', () => { + beforeEach(async () => { + createComponent(); + + await waitForPromises(); + }); + + it('renders the feature settings table and passes the correct props', () => { + expect(findAiFeatureSettingsTable().props('aiFeatureSettings')).toEqual( + mockAiFeatureSettings, + ); + expect(findAiFeatureSettingsTable().props('newSelfHostedModelPath')).toEqual( + '/admin/ai/self_hosted_models/new', + ); + }); + }); + + describe('when the API request is unsuccessful', () => { + const getAiFeatureSettingsErrorHandler = jest.fn().mockResolvedValue({ + data: { + aiFeatureSettings: { + errors: ['An error occured'], + }, + }, + }); + + beforeEach(async () => { + createComponent({ + apolloHandlers: [[getAiFeatureSettingsQuery, getAiFeatureSettingsErrorHandler]], + }); + + await waitForPromises(); + }); + + it('displays an error message', () => { + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'An error occurred while loading the AI feature settings. Please try again.', + }), + ); + }); + }); }); diff --git a/ee/spec/frontend/pages/admin/ai/feature_settings/feature_settings_table_spec.js b/ee/spec/frontend/pages/admin/ai/feature_settings/feature_settings_table_spec.js index 25ef6f69bd61e..32f4bd0d3c19b 100644 --- a/ee/spec/frontend/pages/admin/ai/feature_settings/feature_settings_table_spec.js +++ b/ee/spec/frontend/pages/admin/ai/feature_settings/feature_settings_table_spec.js @@ -1,7 +1,7 @@ import { GlTableLite } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import FeatureSettingsTable from 'ee/pages/admin/ai/feature_settings/components/feature_settings_table.vue'; -import { mockAiFeatureSettings, mockSelfHostedModels } from './mock_data'; +import { mockAiFeatureSettings } from './mock_data'; describe('FeatureSettingsTable', () => { let wrapper; @@ -19,8 +19,7 @@ describe('FeatureSettingsTable', () => { beforeEach(() => { createComponent({ props: { - featureSettings: mockAiFeatureSettings, - models: mockSelfHostedModels, + aiFeatureSettings: mockAiFeatureSettings, newSelfHostedModelPath, }, }); diff --git a/ee/spec/frontend/pages/admin/ai/feature_settings/mock_data.js b/ee/spec/frontend/pages/admin/ai/feature_settings/mock_data.js index eae2a2d9f597b..556cccfccced0 100644 --- a/ee/spec/frontend/pages/admin/ai/feature_settings/mock_data.js +++ b/ee/spec/frontend/pages/admin/ai/feature_settings/mock_data.js @@ -1,29 +1,36 @@ +export const mockSelfHostedModels = [ + { id: 1, name: 'Model 1', model: 'mistral' }, + { id: 2, name: 'Model 2', model: 'codellama' }, + { id: 3, name: 'Model 3', model: 'codegemma' }, +]; + export const mockAiFeatureSettings = [ { feature: 'code_generations', title: 'Code Generation', mainFeature: 'Code Suggestions', - provider: 'self_hosted', + provider: 'vendored', selfHostedModel: null, + validModels: { nodes: mockSelfHostedModels }, }, { feature: 'code_completions', title: 'Code Completion', mainFeature: 'Code Suggestions', - provider: 'vendored', + provider: 'disabled', selfHostedModel: null, + validModels: { nodes: mockSelfHostedModels }, }, { feature: 'duo_chat', title: 'Duo Chat', mainFeature: 'Duo Chat', provider: 'self_hosted', - selfHostedModel: null, + selfHostedModel: { + id: 2, + name: 'Model 2', + model: 'codellama', + }, + validModels: { nodes: mockSelfHostedModels }, }, ]; - -export const mockSelfHostedModels = [ - { id: 1, name: 'Model 1', model: 'mistral' }, - { id: 2, name: 'Model 2', model: 'codellama' }, - { id: 3, name: 'Model 3', model: 'codegemma' }, -]; diff --git a/ee/spec/frontend/pages/admin/ai/feature_settings/model_select_dropdown_spec.js b/ee/spec/frontend/pages/admin/ai/feature_settings/model_select_dropdown_spec.js index 55213eb9781cc..66f33055479a0 100644 --- a/ee/spec/frontend/pages/admin/ai/feature_settings/model_select_dropdown_spec.js +++ b/ee/spec/frontend/pages/admin/ai/feature_settings/model_select_dropdown_spec.js @@ -5,7 +5,7 @@ import { mount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import ModelSelectDropdown from 'ee/pages/admin/ai/feature_settings/components/model_select_dropdown.vue'; -import updateAiFeatureSetting from 'ee/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.graphql'; +import updateAiFeatureSetting from 'ee/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.mutation.graphql'; import { createAlert } from '~/alert'; import { mockSelfHostedModels, mockAiFeatureSettings } from './mock_data'; @@ -38,8 +38,7 @@ describe('ModelSelectDropdown', () => { apolloProvider: mockApollo, propsData: { newSelfHostedModelPath, - featureSetting: mockAiFeatureSetting, - models: mockSelfHostedModels, + aiFeatureSetting: mockAiFeatureSetting, ...props, }, mocks: { diff --git a/ee/spec/requests/admin/ai/feature_settings_controller_spec.rb b/ee/spec/requests/admin/ai/feature_settings_controller_spec.rb index 65d42095733a6..0b5c3c69e6ac8 100644 --- a/ee/spec/requests/admin/ai/feature_settings_controller_spec.rb +++ b/ee/spec/requests/admin/ai/feature_settings_controller_spec.rb @@ -50,20 +50,6 @@ end describe 'GET #index' do - context 'when custom_models_feature_settings_vue_app feature flag is enabled' do - let_it_be(:model) { create(:ai_self_hosted_model) } - - before do - stub_feature_flags(custom_models_feature_settings_vue_app: true) - end - - it 'returns self-hosted models' do - get admin_ai_feature_settings_path - - expect(assigns(:self_hosted_models)).to match_array([model]) - end - end - it 'returns `flagged_features` if ai_duo_chat_sub_features_settings is enabled' do # it expect to go through the ::Ai::FeatureSetting.flagged_features method # So it only shows the stable features diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 22789b0e2dcc8..f1fa136facaf9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3587,6 +3587,12 @@ msgstr "" msgid "AdminAIPoweredFeatures|Add self-hosted model" msgstr "" +msgid "AdminAIPoweredFeatures|An error occurred while loading the AI feature settings. Please try again." +msgstr "" + +msgid "AdminAIPoweredFeatures|Compatible models" +msgstr "" + msgid "AdminAIPoweredFeatures|Disabled" msgstr "" @@ -3602,9 +3608,6 @@ msgstr "" msgid "AdminAIPoweredFeatures|Select a self-hosted model" msgstr "" -msgid "AdminAIPoweredFeatures|Self-hosted models" -msgstr "" - msgid "AdminAIPoweredFeatures|Sub feature" msgstr "" -- GitLab