@swiss-ai-hub/web 0.318.0 → 0.320.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -556,7 +556,9 @@ auth:
556
556
  description: Veuillez vous connecter pour accéder à l'application
557
557
  tagline: Découvrez l'avenir de l'intelligence artificielle
558
558
  welcome: Bienvenue sur le AI Hub de {companyName}
559
+ welcomeProvider: Bienvenue sur le AI Hub — connexion avec {provider}
559
560
  pleaseLogin: Veuillez vous connecter
561
+ pleaseLoginWith: Veuillez vous connecter avec {provider}
560
562
  loginWith: Se connecter avec {provider}
561
563
  customSolutions: Solutions d'IA personnalisées pour votre entreprise
562
564
  logoAlt: Logo de l'entreprise
@@ -795,6 +797,28 @@ form:
795
797
  dans certains cas. Pour éviter tout comportement inattendu, veuillez la désactiver
796
798
  temporairement jusqu'à la publication d'un correctif.
797
799
  lib:
800
+ cron:
801
+ errors:
802
+ required: Chaque position est requise — une position vide ne constitue pas une
803
+ planification valide.
804
+ invalid: Seuls les chiffres et les caractères * , - / ? sont autorisés ici.
805
+ range: Les valeurs doivent être comprises entre {min} et {max}.
806
+ expression: 'Expression cron résultante :'
807
+ minute:
808
+ label: Minute
809
+ hour:
810
+ label: Heure
811
+ day_of_month:
812
+ label: Jour du mois
813
+ month:
814
+ label: Mois
815
+ day_of_week:
816
+ label: Jour de la semaine
817
+ timezone:
818
+ label: Fuseau horaire
819
+ placeholder: Sélectionner un fuseau horaire
820
+ help: Les positions cron sont interprétées dans ce fuseau horaire, afin qu'une
821
+ planification quotidienne conserve la même heure lors des changements d'heure.
798
822
  vectorStore:
799
823
  label: Stockage vectoriel
800
824
  help: Sélectionner la base de connaissances et les espaces de noms à rechercher
@@ -554,7 +554,9 @@ auth:
554
554
  description: Effettua il login per accedere all'applicazione
555
555
  tagline: Sperimenta il futuro dell'intelligenza artificiale
556
556
  welcome: Benvenuti nell'AI Hub di {companyName}
557
+ welcomeProvider: Benvenuti nell'AI Hub — accesso con {provider}
557
558
  pleaseLogin: Effettua il login
559
+ pleaseLoginWith: Effettua il login con {provider}
558
560
  loginWith: Accedi con {provider}
559
561
  customSolutions: Soluzioni AI personalizzate per la tua azienda
560
562
  logoAlt: Logo dell'azienda
@@ -791,6 +793,28 @@ form:
791
793
  in alcuni casi. Per evitare comportamenti inattesi, disattivala temporaneamente
792
794
  fino al rilascio di una correzione.
793
795
  lib:
796
+ cron:
797
+ errors:
798
+ required: Ogni posizione è obbligatoria — una vuota non è una pianificazione
799
+ valida.
800
+ invalid: Qui sono ammessi solo cifre e i caratteri * , - / ? .
801
+ range: I valori devono essere compresi tra {min} e {max}.
802
+ expression: 'Espressione cron risultante:'
803
+ minute:
804
+ label: Minuto
805
+ hour:
806
+ label: Ora
807
+ day_of_month:
808
+ label: Giorno del mese
809
+ month:
810
+ label: Mese
811
+ day_of_week:
812
+ label: Giorno della settimana
813
+ timezone:
814
+ label: Fuso orario
815
+ placeholder: Seleziona un fuso orario
816
+ help: Le posizioni cron sono interpretate in questo fuso orario, così una pianificazione
817
+ giornaliera mantiene la stessa ora durante i cambi di orario.
794
818
  vectorStore:
795
819
  label: Archivio vettoriale
796
820
  help: Seleziona il database di conoscenza e gli spazi dei nomi da cercare
@@ -6,20 +6,42 @@ let lastSyncedTenant: string | null = null
6
6
 
7
7
  const REDIRECT_KEY = 'aihub_redirect_after_login'
8
8
 
9
+ const normalize = (path: string) => (path.endsWith('/') ? path.slice(0, -1) : path)
10
+
11
+ const stripLocale = (path: string, localeCodes: string[]): string => {
12
+ const prefix = localeCodes.find(code => path === `/${code}` || path.startsWith(`/${code}/`))
13
+ return prefix ? path.slice(prefix.length + 1) : path
14
+ }
15
+
16
+ /**
17
+ * The whole login subtree is anonymous, not just the login page itself:
18
+ * per-tenant login links live at `/auth/login/<idp-alias>`. No
19
+ * authenticated-only page may ever be nested below that path.
20
+ *
21
+ * The locale prefix is optional. This middleware runs before @nuxtjs/i18n's
22
+ * `locale-changing` middleware (Nuxt orders file-based global middleware ahead
23
+ * of plugin-registered ones), so bouncing a hand-distributed link that dropped
24
+ * `/en/` would strip the tenant before i18n ever restores the prefix.
25
+ */
26
+ const isAnonymousPath = (path: string, localeCodes: string[]): boolean => {
27
+ const unprefixedPath = stripLocale(normalize(path), localeCodes)
28
+
29
+ return ['/auth/login', '/auth/callback', '/auth/renew'].includes(unprefixedPath)
30
+ || unprefixedPath.startsWith('/auth/login/')
31
+ }
32
+
33
+ const rememberRedirect = (fullPath: string, isAuthPath: boolean) => {
34
+ if (import.meta.client && fullPath !== '/' && !isAuthPath) {
35
+ sessionStorage.setItem(REDIRECT_KEY, fullPath)
36
+ }
37
+ }
38
+
9
39
  export default defineNuxtRouteMiddleware(async (to) => {
10
40
  const { $auth, $i18n } = useNuxtApp()
11
41
  const locale = $i18n.locale.value
42
+ const localeCodes = $i18n.locales.value.map(entry => entry.code)
12
43
 
13
- const noAuthPaths = [
14
- `/${locale}/auth/login`,
15
- `/${locale}/auth/callback`,
16
- `/${locale}/auth/renew`,
17
- ]
18
-
19
- // No auth check for public paths (normalize trailing slashes on both sides)
20
- const normalize = (p: string) => (p.endsWith('/') ? p.slice(0, -1) : p)
21
- const normalizedPath = normalize(to.path)
22
- if (noAuthPaths.some(p => normalize(p) === normalizedPath)) {
44
+ if (isAnonymousPath(to.path, localeCodes)) {
23
45
  return
24
46
  }
25
47
 
@@ -29,9 +51,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
29
51
  const isAuthPath = to.path.includes('/auth/')
30
52
 
31
53
  if (!user) {
32
- if (import.meta.client && to.fullPath !== '/' && !isAuthPath) {
33
- sessionStorage.setItem(REDIRECT_KEY, to.fullPath)
34
- }
54
+ rememberRedirect(to.fullPath, isAuthPath)
35
55
  return navigateTo(`/${locale}/auth/login`)
36
56
  }
37
57
 
@@ -41,9 +61,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
41
61
  }
42
62
  catch {
43
63
  await $auth.removeUser()
44
- if (import.meta.client && to.fullPath !== '/' && !isAuthPath) {
45
- sessionStorage.setItem(REDIRECT_KEY, to.fullPath)
46
- }
64
+ rememberRedirect(to.fullPath, isAuthPath)
47
65
  return navigateTo(`/${locale}/auth/login`)
48
66
  }
49
67
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "license": "AGPL-3.0-or-later",
4
4
  "author": "bbv Software Services AG (https://www.bbv.ch)",
5
5
  "type": "module",
6
- "version": "0.318.0",
6
+ "version": "0.320.0",
7
7
  "description": "Swiss AI Hub - Admin & Management UI (Nuxt 3 layer)",
8
8
  "main": "./nuxt.config.ts",
9
9
  "repository": {
@@ -117,6 +117,9 @@ const submitConfiguration = async (formData: Record<string, unknown>) => {
117
117
  }
118
118
  catch (error) {
119
119
  console.error('Failed to save agent configuration:', error)
120
+ // No `detail` here on purpose: the SDK's global onResponseError (app.vue) already toasts the
121
+ // backend's `detail`, and formats pydantic's array-shaped errors while doing it. Passing
122
+ // `error.message` would only add a second toast reading `[PUT] "<url>": 400 Bad Request`.
120
123
  toast.add({
121
124
  severity: 'error',
122
125
  summary: t('agent.configuration.saveError'),
@@ -96,6 +96,8 @@ const submitConfiguration = async (formData: Record<string, unknown>) => {
96
96
  }
97
97
  catch (error) {
98
98
  console.error('Failed to save process configuration:', error)
99
+ // See the agent configuration page: the SDK's global onResponseError already surfaces the
100
+ // backend `detail`, so adding `error.message` here would only duplicate the toast with a URL.
99
101
  toast.add({
100
102
  severity: 'error',
101
103
  summary: t('process.configuration.saveError'),
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <AuthLoginPanel>
3
+ <template #heading>
4
+ <template v-if="provider">
5
+ {{ t('auth.login.welcomeProvider', { provider: provider.display_name }) }}
6
+ </template>
7
+ </template>
8
+ <template #message>
9
+ <template v-if="provider">
10
+ {{ t('auth.login.pleaseLoginWith', { provider: provider.display_name }) }}
11
+ </template>
12
+ </template>
13
+
14
+ <Button
15
+ v-if="provider"
16
+ :label="t('auth.login.loginWith', { provider: provider.display_name })"
17
+ :icon="`pi ${provider.icon}`"
18
+ icon-pos="right"
19
+ class="!bg-white !text-black"
20
+ @click="login(provider.alias)"
21
+ />
22
+ <ProgressSpinner
23
+ v-else
24
+ class="!h-8 !w-8"
25
+ />
26
+ </AuthLoginPanel>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ definePageMeta({
31
+ layout: 'anonymous',
32
+ })
33
+
34
+ const { t, locale } = useI18n()
35
+ const route = useRoute()
36
+ const { login } = useAuth()
37
+ const { authProviders, isLoading } = useAuthProviders()
38
+
39
+ /**
40
+ * The empty alias belongs to the synthetic "Keycloak" entry, which has no
41
+ * kc_idp_hint and must therefore not be addressable through a tenant link.
42
+ */
43
+ const requestedAlias = computed(() => String(route.params.idp ?? ''))
44
+ const provider = computed(() =>
45
+ requestedAlias.value
46
+ ? authProviders.value?.find(candidate => candidate.alias === requestedAlias.value)
47
+ : undefined,
48
+ )
49
+
50
+ // Only decide once the query settled — an unknown, disabled or hidden alias
51
+ // (and a failed provider request) falls back to the all-providers page.
52
+ watch([isLoading, provider], ([providersLoading, matchedProvider]) => {
53
+ if (!providersLoading && !matchedProvider) {
54
+ navigateTo(`/${locale.value}/auth/login`, { replace: true })
55
+ }
56
+ }, { immediate: true })
57
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <AuthLoginPanel>
3
+ <template #heading>
4
+ {{ t('auth.login.welcome', { companyName }) }}
5
+ </template>
6
+ <template #message>
7
+ {{ t('auth.login.pleaseLogin') }}
8
+ </template>
9
+
10
+ <ProgressSpinner
11
+ v-if="isLoading"
12
+ class="!h-8 !w-8"
13
+ />
14
+ <template v-else>
15
+ <Button
16
+ v-for="idp in authProviders ?? []"
17
+ :key="idp.alias"
18
+ :label="t('auth.login.loginWith', { provider: idp.display_name })"
19
+ :icon="`pi ${idp.icon}`"
20
+ icon-pos="right"
21
+ class="!bg-white !text-black"
22
+ @click="login(idp.alias || undefined)"
23
+ />
24
+ <Button
25
+ v-if="(authProviders?.length ?? 0) === 0"
26
+ :label="t('auth.login.title')"
27
+ icon="pi pi-sign-in"
28
+ icon-pos="right"
29
+ class="!bg-white !text-black"
30
+ @click="login()"
31
+ />
32
+ </template>
33
+ </AuthLoginPanel>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ definePageMeta({
38
+ layout: 'anonymous',
39
+ })
40
+
41
+ const { t } = useI18n()
42
+ const { login } = useAuth()
43
+ const { authProviders, isLoading } = useAuthProviders()
44
+
45
+ const companyName = 'bbv Software Services AG'
46
+ </script>
@@ -197,9 +197,7 @@ export const createClient = (config: Config = {}): Client => {
197
197
  method,
198
198
  onRequest: undefined,
199
199
  serializedBody: getValidRequestBody(opts) as
200
- | BodyInit
201
- | null
202
- | undefined,
200
+ BodyInit | null | undefined,
203
201
  signal: unwrapRefs(opts.signal) as AbortSignal,
204
202
  url,
205
203
  });
@@ -33,14 +33,12 @@ export type QuerySerializer = (
33
33
 
34
34
  type WithRefs<TData> = {
35
35
  [K in keyof TData]: NonNullable<TData[K]> extends object
36
- ?
37
- | WithRefs<NonNullable<TData[K]>>
38
- | Ref<NonNullable<TData[K]>>
39
- | Extract<TData[K], null>
40
- :
41
- | NonNullable<TData[K]>
42
- | Ref<NonNullable<TData[K]>>
43
- | Extract<TData[K], null>;
36
+ ? | WithRefs<NonNullable<TData[K]>>
37
+ | Ref<NonNullable<TData[K]>>
38
+ | Extract<TData[K], null>
39
+ : | NonNullable<TData[K]>
40
+ | Ref<NonNullable<TData[K]>>
41
+ | Extract<TData[K], null>;
44
42
  };
45
43
 
46
44
  // copied from Nuxt
@@ -206,8 +204,4 @@ type FetchOptions<TData> = Omit<
206
204
  >;
207
205
 
208
206
  export type Composable =
209
- | "$fetch"
210
- | "useAsyncData"
211
- | "useFetch"
212
- | "useLazyAsyncData"
213
- | "useLazyFetch";
207
+ "$fetch" | "useAsyncData" | "useFetch" | "useLazyAsyncData" | "useLazyFetch";
@@ -195,8 +195,7 @@ export const setAuthParams = async ({
195
195
  options.query = {};
196
196
  }
197
197
  const queryValue = toValue(options.query) as
198
- | Record<string, unknown>
199
- | undefined;
198
+ Record<string, unknown> | undefined;
200
199
  if (queryValue) {
201
200
  queryValue[name] = token;
202
201
  }
@@ -4,12 +4,7 @@
4
4
  * JSON-friendly union that mirrors what Pinia Colada can hash.
5
5
  */
6
6
  export type JsonValue =
7
- | null
8
- | string
9
- | number
10
- | boolean
11
- | JsonValue[]
12
- | { [key: string]: JsonValue };
7
+ null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue };
13
8
 
14
9
  /**
15
10
  * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
@@ -112,7 +112,9 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
112
112
  : false;
113
113
 
114
114
  export type OmitNever<T extends Record<string, unknown>> = {
115
- [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
116
- ? never
117
- : K]: T[K];
115
+ [
116
+ K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
117
+ ? never
118
+ : K
119
+ ]: T[K];
118
120
  };
@@ -50,6 +50,8 @@ export {
50
50
  getLitellmModel,
51
51
  getLitellmModels,
52
52
  getLitellmModelsByMode,
53
+ getLlmSpendByTenant,
54
+ getLlmSpendByUser,
53
55
  getLocale,
54
56
  getModels,
55
57
  getModelWithAssistants,
@@ -313,6 +315,10 @@ export {
313
315
  type CreateTranscriptionErrors,
314
316
  type CreateTranscriptionResponse,
315
317
  type CreateTranscriptionResponses,
318
+ type CronInput,
319
+ type CronInputWritable,
320
+ type CronStartEvent,
321
+ type CronStartEventWritable,
316
322
  type CustomOutput,
317
323
  type DashboardDto,
318
324
  type DashboardItemDto,
@@ -524,6 +530,16 @@ export {
524
530
  type GetLitellmModelsData,
525
531
  type GetLitellmModelsResponse,
526
532
  type GetLitellmModelsResponses,
533
+ type GetLlmSpendByTenantData,
534
+ type GetLlmSpendByTenantError,
535
+ type GetLlmSpendByTenantErrors,
536
+ type GetLlmSpendByTenantResponse,
537
+ type GetLlmSpendByTenantResponses,
538
+ type GetLlmSpendByUserData,
539
+ type GetLlmSpendByUserError,
540
+ type GetLlmSpendByUserErrors,
541
+ type GetLlmSpendByUserResponse,
542
+ type GetLlmSpendByUserResponses,
527
543
  type GetLocaleData,
528
544
  type GetLocaleResponse,
529
545
  type GetLocaleResponses,
@@ -731,6 +747,7 @@ export {
731
747
  type LlmCostEventWritable,
732
748
  type LlmEvent,
733
749
  type LlmEventWritable,
750
+ type LlmSpend,
734
751
  type LlmStopEvent,
735
752
  type LlmStopEventWritable,
736
753
  type LocaleInput,
@@ -739,10 +756,14 @@ export {
739
756
  type LocaleString,
740
757
  type Logprob,
741
758
  type MailAttachmentRef,
759
+ type MailBatchClassifiedEvent,
760
+ type MailBatchClassifiedEventWritable,
742
761
  type MailBatchDraftedEvent,
743
762
  type MailBatchDraftedEventWritable,
763
+ type MailClassificationRef,
744
764
  type MailFetchedEvent,
745
765
  type MailFetchedEventWritable,
766
+ type MailMessageRef,
746
767
  type MailMovedEvent,
747
768
  type MailMovedEventWritable,
748
769
  type MemoriesResponse,