Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions api/doc/settings/org-form-part.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import OrgPutReq from './org-put-req/schema.js'

/**
* The org config page shows its form in tabs, one vjsf form per tab. Each tab
* The org config page shows its form in tabs, one vjsf form per tab (except
* quotas, edited as a plain table by QuotasTable.vue). Each tab
* schema is a slice of the org PUT body schema (the single source of truth,
* still used to validate the whole body server side), so they cannot drift.
* The page composes the slices back into the full body before saving.
*
* @param {string} name
* @param {('modelMapping' | 'quotas' | 'moderation' | 'storeTraces')[]} keys
* @param {('modelMapping' | 'moderation' | 'storeTraces')[]} keys
*/
export const orgFormPart = (name, keys) => {
/** @type {Record<string, any>} */
Expand Down
1 change: 0 additions & 1 deletion api/doc/settings/org-form-quotas/index.d.ts

This file was deleted.

1 change: 0 additions & 1 deletion api/doc/settings/org-form-quotas/index.js

This file was deleted.

3 changes: 0 additions & 3 deletions api/doc/settings/org-form-quotas/schema.js

This file was deleted.

24 changes: 14 additions & 10 deletions tests/features/settings/org-settings.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { test } from '../../fixtures/login.ts'
import { clean, superAdmin } from '../../support/axios.ts'
import { putSettings } from '../../support/settings.ts'

// the form is split in tabs (models / quotas / moderation and traces)
// the form is split in tabs (quotas / moderation and traces / models)
const openTab = (page: Page, name: string) => page.locator('#configuration').getByRole('tab', { name }).click()

const seedProviderAndModel = async () => {
Expand Down Expand Up @@ -45,6 +45,15 @@ test.describe('Org admin config UI', () => {
// "succeed" without the edits having registered.
await expect(page.getByRole('button', { name: 'Save' })).not.toBeVisible()

// A per-profile credit quota, shown against the account-wide allowance
// (unlimited in dev: DEFAULT_CREDITS=-1)
await expect(page.getByText('Account global limit: unlimited.')).toBeVisible()
await page.getByRole('spinbutton', { name: 'Contributors - Monthly limit (AI credits)' }).fill('42')

// Model mapping comes last: the defaults are the recommended choice
await openTab(page, 'Model per role')
await expect(page.getByText('Leave the roles empty to use the default models')).toBeVisible()

// An unmapped role shows the model it falls back to, so leaving it empty
// does not read as missing information
await expect(page.getByRole('combobox', { name: 'Assistant' })).toHaveAttribute('placeholder', 'Default: Global Mock Model (Global Mock)')
Expand All @@ -56,10 +65,6 @@ test.describe('Org admin config UI', () => {
await expect(page.getByRole('option', { name: 'Global Mock Model (Global Mock)' })).toBeVisible()
await page.getByRole('option', { name: 'Mock Model (Mock Provider)' }).click()

// A per-profile credit quota
await openTab(page, 'Quotas')
await page.getByRole('textbox', { name: 'Monthly Limit' }).first().fill('42')

// And the store-traces switch
await openTab(page, 'Moderation and traces')
await page.getByText('Store conversation traces').click()
Expand All @@ -72,9 +77,9 @@ test.describe('Org admin config UI', () => {
await expect(page.getByRole('tab', { name: 'Model per role' })).toBeVisible({ timeout: 15000 })
// The stored ref is matched against the freshly fetched catalog and shown as
// the autocomplete's selection (a slot, not the input's value).
await expect(page.getByRole('spinbutton', { name: 'Contributors - Monthly limit (AI credits)' })).toHaveValue('42')
await openTab(page, 'Model per role')
await expect(page.getByText('Mock Model (Mock Provider)')).toBeVisible()
await openTab(page, 'Quotas')
await expect(page.getByRole('textbox', { name: 'Monthly Limit' }).first()).toHaveValue('42')
await openTab(page, 'Moderation and traces')
await expect(page.getByRole('checkbox', { name: 'Store conversation traces' })).toBeChecked()
await page.waitForTimeout(800)
Expand Down Expand Up @@ -122,9 +127,8 @@ test.describe('Org admin config UI', () => {
await goToWithAuth('/agents/user/test-standalone1', 'test-standalone1')
await expect(page.getByRole('tab', { name: 'Model per role' })).toBeVisible({ timeout: 15000 })
await page.waitForTimeout(500)
await openTab(page, 'Quotas')
await expect(page.getByText('Admin quotas')).toBeVisible()
await expect(page.getByText('Contributor quotas')).not.toBeVisible()
await expect(page.getByRole('cell', { name: 'Admins', exact: true })).toBeVisible()
await expect(page.getByRole('cell', { name: 'Contributors', exact: true })).not.toBeVisible()

await openTab(page, 'Moderation and traces')
await page.getByText('Store conversation traces').click()
Expand Down
7 changes: 1 addition & 6 deletions ui/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ declare module 'vue' {
MonitoringPlatformSection: typeof import('./src/components/MonitoringPlatformSection.vue')['default']
MonitoringUserHistogram: typeof import('./src/components/MonitoringUserHistogram.vue')['default']
OrgConfigSection: typeof import('./src/components/OrgConfigSection.vue')['default']
QuotasTable: typeof import('./src/components/QuotasTable.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TraceComparePicker: typeof import('./src/components/TraceComparePicker.vue')['default']
Expand All @@ -42,12 +43,6 @@ declare module 'vue' {
VjsfOrgFormModeration: typeof import('./src/components/vjsf/vjsf-org-form-moderation.vue')['default']
VjsfOrgFormModerationEn: typeof import('./src/components/vjsf/vjsf-org-form-moderation-en.vue')['default']
VjsfOrgFormModerationFr: typeof import('./src/components/vjsf/vjsf-org-form-moderation-fr.vue')['default']
VjsfOrgFormQuotas: typeof import('./src/components/vjsf/vjsf-org-form-quotas.vue')['default']
VjsfOrgFormQuotasEn: typeof import('./src/components/vjsf/vjsf-org-form-quotas-en.vue')['default']
VjsfOrgFormQuotasFr: typeof import('./src/components/vjsf/vjsf-org-form-quotas-fr.vue')['default']
VjsfOrgPutReq: typeof import('./src/components/vjsf/vjsf-org-put-req.vue')['default']
VjsfOrgPutReqEn: typeof import('./src/components/vjsf/vjsf-org-put-req-en.vue')['default']
VjsfOrgPutReqFr: typeof import('./src/components/vjsf/vjsf-org-put-req-fr.vue')['default']
VjsfPutReq: typeof import('./src/components/vjsf/vjsf-put-req.vue')['default']
VjsfPutReqEn: typeof import('./src/components/vjsf/vjsf-put-req-en.vue')['default']
VjsfPutReqFr: typeof import('./src/components/vjsf/vjsf-put-req-fr.vue')['default']
Expand Down
63 changes: 33 additions & 30 deletions ui/src/components/OrgConfigSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,46 @@
<!-- eager: every tab's form stays mounted, so the validity of the tabs
not currently shown still gates the save -->
<v-tabs-window-item
value="models"
value="quotas"
eager
>
<p class="text-body-medium mb-4">
{{ t('modelsHint') }}
</p>
<v-form v-model="tabValid.models">
<vjsf-org-form-models
:model-value="parts.models"
:options="vjsfOptions"
:locale="locale"
@update:model-value="(data: any) => onPartUpdate('models', data)"
<v-form v-model="tabValid.quotas">
<quotas-table
:model-value="parts.quotas.quotas"
:account-type="accountType"
:account-id="accountId"
@update:model-value="(quotas) => onPartUpdate('quotas', { quotas })"
/>
</v-form>
</v-tabs-window-item>
<v-tabs-window-item
value="quotas"
value="moderation"
eager
>
<v-form v-model="tabValid.quotas">
<vjsf-org-form-quotas
:model-value="parts.quotas"
<v-form v-model="tabValid.moderation">
<vjsf-org-form-moderation
:model-value="parts.moderation"
:options="vjsfOptions"
:locale="locale"
@update:model-value="(data: any) => onPartUpdate('quotas', data)"
@update:model-value="(data: any) => onPartUpdate('moderation', data)"
/>
</v-form>
</v-tabs-window-item>
<v-tabs-window-item
value="moderation"
value="models"
eager
>
<v-form v-model="tabValid.moderation">
<vjsf-org-form-moderation
:model-value="parts.moderation"
<df-tutorial-alert
id="agents-config-models-defaults"
:text="t('modelsHint')"
persistent
/>
<v-form v-model="tabValid.models">
<vjsf-org-form-models
:model-value="parts.models"
:options="vjsfOptions"
:locale="locale"
@update:model-value="(data: any) => onPartUpdate('moderation', data)"
@update:model-value="(data: any) => onPartUpdate('models', data)"
/>
</v-form>
</v-tabs-window-item>
Expand All @@ -68,23 +70,23 @@
<i18n lang="yaml">
fr:
title: Configuration
subtitle: Modèles utilisés, quotas de consommation par profil d'utilisateur, modération et enregistrement des conversations.
subtitle: Quotas de consommation par profil d'utilisateur, modération, enregistrement des conversations et modèles utilisés.
saved: Les modifications ont été enregistrées
tabs:
models: Modèle par rôle
quotas: Quotas
moderation: Modération et traces
modelsHint: Laissez un rôle vide pour utiliser le modèle par défaut affiché dans le champ, ce qui est le choix recommandé dans la plupart des cas.
modelsHint: Laissez les rôles vides pour utiliser les modèles par défaut affichés dans les champs. C'est le choix recommandé, ne sélectionnez un autre modèle que si vous avez une raison précise de le faire.
defaultModel: "Par défaut : {model}"
en:
title: Configuration
subtitle: Models in use, consumption quotas per user profile, moderation and conversation storage.
subtitle: Consumption quotas per user profile, moderation, conversation storage and models in use.
saved: Changes have been saved
tabs:
models: Model per role
quotas: Quotas
moderation: Moderation and traces
modelsHint: Leave a role empty to use the default model shown in the field, which is the recommended choice in most cases.
modelsHint: Leave the roles empty to use the default models shown in the fields. This is the recommended choice, only select another model if you have a specific reason to.
defaultModel: "Default: {model}"
</i18n>

Expand All @@ -97,7 +99,8 @@ import { useFetch } from '@data-fair/lib-vue/fetch.js'
import DfSectionTabs from '@data-fair/lib-vuetify/section-tabs.vue'
import type { Settings } from '#api/types'
import VjsfOrgFormModels from '~/components/vjsf/vjsf-org-form-models.vue'
import VjsfOrgFormQuotas from '~/components/vjsf/vjsf-org-form-quotas.vue'
import QuotasTable from '~/components/QuotasTable.vue'
import DfTutorialAlert from '@data-fair/lib-vuetify/tutorial-alert.vue'
import VjsfOrgFormModeration from '~/components/vjsf/vjsf-org-form-moderation.vue'
import FormActions from '~/components/FormActions.vue'
import { useSettingsForm } from '~/composables/use-settings-form'
Expand Down Expand Up @@ -202,9 +205,9 @@ watch(() => settingsFetch.data.value?.updatedAt, (_, previous) => {
*/
type TabKey = 'models' | 'quotas' | 'moderation'
const TAB_FIELDS: Record<TabKey, (keyof OrgOwnedSettings)[]> = {
models: ['modelMapping'],
quotas: ['quotas'],
moderation: ['moderation', 'storeTraces']
moderation: ['moderation', 'storeTraces'],
models: ['modelMapping']
}
const tabKeys = Object.keys(TAB_FIELDS) as TabKey[]

Expand All @@ -229,11 +232,11 @@ const onPartUpdate = (tab: TabKey, data: Partial<OrgOwnedSettings>) => {
const tabValid = ref<Record<TabKey, boolean | null>>({ models: null, quotas: null, moderation: null })
const valid = computed(() => tabKeys.every(tab => tabValid.value[tab] !== false))

const tab = ref<TabKey>('models')
const tab = ref<TabKey>('quotas')
const tabs = computed(() => ([
{ key: 'models', icon: mdiRobotOutline },
{ key: 'quotas', icon: mdiGauge },
{ key: 'moderation', icon: mdiShieldCheckOutline }
{ key: 'moderation', icon: mdiShieldCheckOutline },
{ key: 'models', icon: mdiRobotOutline }
] as const).map(({ key, icon }) => ({
key,
icon,
Expand Down
Loading
Loading