@swiss-ai-hub/web 0.321.1 → 0.323.1

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.
Files changed (100) hide show
  1. package/components/Event/Display/AddMemoryToChatHistoryEvent.vue +3 -3
  2. package/components/Event/Display/AttachedFileEvent.vue +42 -0
  3. package/components/Event/Display/AttachedFilesReadEvent.vue +22 -0
  4. package/components/Event/Display/BaseRetrieveMemoryEvent.vue +2 -2
  5. package/components/Event/Display/BaseStoreMemoryEvent.vue +2 -2
  6. package/components/Event/Display/CompleteConversationEvent.vue +35 -0
  7. package/components/Event/Display/ComposeContextEvent.vue +38 -0
  8. package/components/Event/Display/ContextComposedEvent.vue +60 -0
  9. package/components/Event/Display/ContextualizeConversationEvent.vue +26 -0
  10. package/components/Event/Display/ConversationContextualizedEvent.vue +34 -0
  11. package/components/Event/Display/HumanInTheLoopResponseEvent.vue +11 -1
  12. package/components/Event/Display/KnowledgeSearchedEvent.vue +33 -0
  13. package/components/Event/Display/MemoryRecalledEvent.vue +33 -0
  14. package/components/Event/Display/MemoryStorageRequestedEvent.vue +29 -0
  15. package/components/Event/Display/NotAMetaQuestionEvent.vue +20 -0
  16. package/components/Event/Display/Part/Facts.vue +39 -0
  17. package/components/Event/Display/Part/MessageList.vue +50 -0
  18. package/components/Event/Display/Part/ToolCalls.vue +58 -0
  19. package/components/Event/Display/Part/ToolLoopState.vue +36 -0
  20. package/components/Event/Display/ReadAttachedFilesEvent.vue +28 -0
  21. package/components/Event/Display/RecallMemoryEvent.vue +27 -0
  22. package/components/Event/Display/RunToolLoopEvent.vue +34 -0
  23. package/components/Event/Display/SandboxFileDisplayedEvent.vue +46 -0
  24. package/components/Event/Display/SearchKnowledgeEvent.vue +31 -0
  25. package/components/Event/Display/ToolCallApprovedEvent.vue +27 -0
  26. package/components/Event/Display/ToolCallsDecidedEvent.vue +24 -0
  27. package/components/Event/Display/ToolLoopCondensedEvent.vue +31 -0
  28. package/components/Event/Display/ToolLoopFinishedEvent.vue +49 -0
  29. package/components/Event/Display/ToolLoopIterationEvent.vue +18 -0
  30. package/components/Event/Display/ToolLoopStatusEvent.vue +27 -0
  31. package/components/Event/Display/ToolResultEvent.vue +38 -0
  32. package/components/Event/List/EventList.vue +1 -1
  33. package/components/FormKit/DynamicConfiguration.vue +6 -1
  34. package/components/FormKit/KnowledgeCollectionSelector.vue +281 -0
  35. package/components/FormKit/KnowledgeDatabaseSelector.vue +7 -0
  36. package/components/FormKit/LocaleInput.vue +2 -0
  37. package/components/FormKit/SecretFileInput.vue +181 -0
  38. package/components/FormKit/VectorStoreInput.vue +20 -1
  39. package/components/Knowledge/Database/CreateModal.vue +91 -2
  40. package/components/Knowledge/Database/SourceModal.vue +165 -0
  41. package/components/Knowledge/Document/UploadModal.vue +4 -0
  42. package/components/Knowledge/Namespace/Card.vue +4 -4
  43. package/components/Knowledge/Namespace/CreateModal.vue +1 -1
  44. package/components/Knowledge/Namespace/EditModal.vue +3 -4
  45. package/components/Role/AccessRulesEditor.vue +1 -1
  46. package/components/Service/Selection.vue +7 -1
  47. package/components/Support/IncidentButton.vue +37 -0
  48. package/components/Support/IncidentDialog.vue +119 -0
  49. package/components/User/Settings.vue +9 -8
  50. package/components/UserFiles/Browser.vue +361 -0
  51. package/components/UserFiles/MoveDialog.vue +77 -0
  52. package/components/UserFiles/NameDialog.vue +59 -0
  53. package/components/UserFiles/Preview.vue +110 -0
  54. package/composables/auth/useAuth.ts +19 -3
  55. package/composables/document/useDocumentIngestionPolling.ts +64 -0
  56. package/composables/document/useDocuments.ts +3 -0
  57. package/composables/document/useSourcePipelines.ts +23 -0
  58. package/composables/document/useUpdateDatabaseSource.ts +21 -0
  59. package/composables/event/useEventComponent.ts +46 -0
  60. package/composables/form/useFormKitTransform.ts +68 -17
  61. package/composables/openwebui/useOpenWebuiDisclaimer.ts +22 -0
  62. package/composables/suite/useApps.ts +12 -3
  63. package/composables/suite/useSuite.ts +2 -0
  64. package/composables/support/useIncidentAvailability.ts +17 -0
  65. package/composables/support/useIncidentContext.ts +56 -0
  66. package/composables/support/useIncidentForm.ts +45 -0
  67. package/composables/support/useIncidentReport.ts +16 -0
  68. package/composables/support/useOpenWebUIContext.ts +38 -0
  69. package/composables/tenant/useChatDisclaimer.ts +24 -0
  70. package/composables/user/useApplyLocale.ts +23 -0
  71. package/composables/user/useMyLocale.ts +20 -0
  72. package/composables/user/useRestorePreferredLocale.ts +38 -0
  73. package/composables/userFiles/useCreateUserFolder.ts +15 -0
  74. package/composables/userFiles/useDeleteUserFile.ts +15 -0
  75. package/composables/userFiles/useMoveUserFile.ts +15 -0
  76. package/composables/userFiles/useUploadUserFiles.ts +19 -0
  77. package/composables/userFiles/useUserFileContent.ts +24 -0
  78. package/composables/userFiles/useUserFiles.ts +25 -0
  79. package/formkit.config.ts +10 -2
  80. package/i18n/locales/de.yaml +171 -3
  81. package/i18n/locales/en.yaml +172 -3
  82. package/i18n/locales/fr.yaml +173 -3
  83. package/i18n/locales/it.yaml +173 -3
  84. package/layouts/default.vue +7 -1
  85. package/middleware/auth.global.ts +6 -3
  86. package/middleware/home-redirect.ts +15 -4
  87. package/package.json +4 -3
  88. package/pages/[tenant]/service/knowledge/[db]/[namespace].vue +6 -8
  89. package/pages/[tenant]/service/knowledge.vue +33 -7
  90. package/pages/[tenant]/service/openai/[thread_id]/[display_id]/files.vue +51 -0
  91. package/pages/[tenant]/service/openai.vue +61 -17
  92. package/pages/[tenant]/service/user-knowledge.vue +19 -0
  93. package/plugins/api-client.client.ts +5 -0
  94. package/plugins/oidc-client.ts +14 -7
  95. package/sdk/client/index.ts +171 -1
  96. package/sdk/client/schemas.gen.ts +18602 -8560
  97. package/sdk/client/sdk.gen.ts +482 -0
  98. package/sdk/client/types.gen.ts +13616 -7044
  99. package/types/EventFact.ts +4 -0
  100. package/utils/apiResponseGuard.ts +97 -0
@@ -13,9 +13,9 @@
13
13
  <div class="flex items-center gap-2 pb-2 pl-2">
14
14
  <span class="text-sm font-medium">{{ database.display_name || capitalCase(database.name) }}</span>
15
15
  <i
16
- v-if="database.auto_sync"
17
- class="pi pi-lock text-surface-400 dark:text-surface-500"
18
- :title="t('knowledge.auto_sync.description')"
16
+ v-if="database.source"
17
+ class="pi pi-sync text-surface-400 dark:text-surface-500"
18
+ :title="t('knowledge.source.description', { name: capitalCase(database.source) })"
19
19
  />
20
20
  <i
21
21
  v-else
@@ -25,6 +25,16 @@
25
25
  <span class="text-xs text-surface-500 dark:text-surface-400">
26
26
  {{ t('knowledge.pipeline', { name: capitalCase(database.ingestor) }) }}
27
27
  </span>
28
+ <Button
29
+ v-if="database.deletable"
30
+ v-tooltip.top="t('knowledge.source.edit')"
31
+ icon="pi pi-cloud-download"
32
+ rounded
33
+ text
34
+ size="small"
35
+ severity="secondary"
36
+ @click="openSourceModal(database)"
37
+ />
28
38
  <Button
29
39
  v-if="database.deletable"
30
40
  v-tooltip.top="t('knowledge.delete_database')"
@@ -41,14 +51,14 @@
41
51
  v-for="namespace in database.namespaces"
42
52
  :key="namespace.name"
43
53
  :namespace="namespace"
44
- :auto-sync="database.auto_sync"
54
+ :sourced="!!database.source"
45
55
  @click="toNamespace(database.name, namespace)"
46
56
  @upload="openUploadModal(database, namespace)"
47
- @edit="openEditNamespaceModal(namespace)"
57
+ @edit="openEditNamespaceModal(database, namespace)"
48
58
  @delete="openDeleteNamespaceModal(database, namespace)"
49
59
  />
50
60
  <KnowledgeNamespaceEmptyCard
51
- v-if="!database.auto_sync"
61
+ v-if="!database.source"
52
62
  @add="openNewNamespaceModal(database.name)"
53
63
  />
54
64
  </div>
@@ -75,6 +85,7 @@
75
85
 
76
86
  <KnowledgeNamespaceEditModal
77
87
  v-model="editNamespaceModalVisible"
88
+ :database="editingDatabase"
78
89
  :namespace="editingNamespace"
79
90
  @success="handleUpdateSuccess"
80
91
  />
@@ -84,6 +95,11 @@
84
95
  @success="handleDatabaseCreationSuccess"
85
96
  />
86
97
 
98
+ <KnowledgeDatabaseSourceModal
99
+ v-model="sourceModalVisible"
100
+ :database="selectedDatabaseForSource"
101
+ />
102
+
87
103
  <KnowledgeDeleteConfirmModal
88
104
  v-model:visible="deleteModalVisible"
89
105
  :title="deleteTitle"
@@ -122,10 +138,19 @@ const newNamespaceModalVisible = ref(false)
122
138
  const selectedDatabaseForNewNamespace = ref('')
123
139
 
124
140
  const editNamespaceModalVisible = ref(false)
141
+ const editingDatabase = ref('')
125
142
  const editingNamespace = ref<NamespaceDto | null>(null)
126
143
 
127
144
  const newDatabaseModalVisible = ref(false)
128
145
 
146
+ const sourceModalVisible = ref(false)
147
+ const selectedDatabaseForSource = ref<DatabaseDto | null>(null)
148
+
149
+ const openSourceModal = (database: DatabaseDto) => {
150
+ selectedDatabaseForSource.value = database
151
+ sourceModalVisible.value = true
152
+ }
153
+
129
154
  const toNamespace = (database_name: string, namespace: NamespaceDto) => {
130
155
  router.push(tenantPath(`/service/knowledge/${database_name}/${namespace.name}`))
131
156
  }
@@ -152,7 +177,8 @@ const handleCreationSuccess = (data: { database: string, namespace: string }) =>
152
177
  router.push(tenantPath(`/service/knowledge/${data.database}/${data.namespace}`))
153
178
  }
154
179
 
155
- const openEditNamespaceModal = (namespace: NamespaceDto) => {
180
+ const openEditNamespaceModal = (database: DatabaseDto, namespace: NamespaceDto) => {
181
+ editingDatabase.value = database.name
156
182
  editingNamespace.value = namespace
157
183
  editNamespaceModalVisible.value = true
158
184
  }
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div
3
+ ref="filesPanelRef"
4
+ class="relative h-[calc(100vh-50px)] w-1/2 min-w-[800px] overflow-y-auto border-l border-surface-200 p-6 dark:border-surface-700"
5
+ >
6
+ <div class="mb-4">
7
+ <h2 class="text-2xl font-semibold">
8
+ {{ t('userFiles.title') }}
9
+ </h2>
10
+ </div>
11
+
12
+ <UserFilesBrowser
13
+ :initial-folder="`conversations/${threadId}`"
14
+ stacked
15
+ />
16
+
17
+ <div
18
+ class="fixed top-1/2 -translate-y-1/2"
19
+ :style="{ left: `${panelLeftPosition - 16}px` }"
20
+ >
21
+ <button
22
+ v-tooltip.left="t('userFiles.actions.close')"
23
+ type="button"
24
+ :aria-label="t('userFiles.actions.close')"
25
+ class="flex cursor-pointer rounded-full border border-surface-200 bg-surface-0 p-3 hover:bg-surface-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary dark:border-surface-700 dark:bg-surface-900 hover:dark:bg-surface-800"
26
+ @click="closeFiles"
27
+ >
28
+ <i
29
+ class="pi pi-chevron-right"
30
+ aria-hidden="true"
31
+ />
32
+ </button>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ const route = useRoute()
39
+ const router = useRouter()
40
+ const tenantPath = useTenantPath()
41
+ const { t } = useI18n()
42
+
43
+ const threadId = computed(() => route.params.thread_id as string)
44
+ const filesPanelRef = ref(null)
45
+ const panelBounding = useElementBounding(filesPanelRef)
46
+ const panelLeftPosition = computed(() => panelBounding.left.value)
47
+
48
+ const closeFiles = () => {
49
+ router.push(tenantPath('/service/openai'))
50
+ }
51
+ </script>
@@ -2,6 +2,7 @@
2
2
  <div class="flex flex-row">
3
3
  <div class="h-[calc(100vh-50px)] w-full">
4
4
  <iframe
5
+ ref="chatIframe"
5
6
  :src="`${runtimeConfig.public.webui.url}/oauth/oidc/login`"
6
7
  width="100%"
7
8
  height="100%"
@@ -42,6 +43,15 @@ const tenantPath = useTenantPath()
42
43
  const localePath = useLocalePath()
43
44
  const { mismatchDetected, backendTenantId, backendTenantName } = useTenantPolling()
44
45
  const { setTenant, tenantId } = useTenant()
46
+ const { setOpenWebUIContext, updateOpenWebUIContext, clearOpenWebUIContext } = useOpenWebUIContext()
47
+ const chatIframe = ref<HTMLIFrameElement | null>(null)
48
+ const { disclaimer } = useChatDisclaimer()
49
+ const visibleDisclaimer = computed(() => {
50
+ if (backendTenantId.value && backendTenantId.value !== tenantId.value) return ''
51
+ // Use generic text until OpenWebUI's active tenant has been confirmed.
52
+ return backendTenantId.value === tenantId.value ? disclaimer.value : t('openwebui.default_disclaimer')
53
+ })
54
+ const { sendDisclaimer } = useOpenWebuiDisclaimer(chatIframe, visibleDisclaimer)
45
55
 
46
56
  async function onSwitchToBackendTenant() {
47
57
  if (!backendTenantId.value) return
@@ -51,6 +61,7 @@ async function onSwitchToBackendTenant() {
51
61
  let initialLoadDone = false
52
62
 
53
63
  const handleIframeLoad = () => {
64
+ sendDisclaimer()
54
65
  // Skip the initial load when iframe first renders OpenWebUI
55
66
  if (!initialLoadDone) {
56
67
  initialLoadDone = true
@@ -69,21 +80,42 @@ const VIEW_BY_ACTION: Record<string, string> = {
69
80
  'show-traces': 'tracing',
70
81
  'show-sources': 'sources',
71
82
  'show-memories': 'memories',
83
+ 'show-files': 'files',
72
84
  }
73
85
 
86
+ const HANDLED_MESSAGE_TYPES = new Set([...Object.keys(VIEW_BY_ACTION), 'set-model-context', 'set-context'])
87
+
74
88
  const openPanelView = (): string | null => {
75
89
  if (route.path.endsWith('/tracing')) return 'tracing'
76
90
  if (route.path.endsWith('/sources')) return 'sources'
77
91
  if (route.path.endsWith('/memories')) return 'memories'
92
+ if (route.path.endsWith('/files')) return 'files'
78
93
  return null
79
94
  }
80
95
 
96
+ // The action inside the iframe knows only the display, so the owning thread — the correct
97
+ // per-agent, salted one the pipe persisted events under — comes from the backend. Empty when no
98
+ // AI-Hub thread owns the display, which a plain-LLM message never does.
99
+ const resolveThreadId = async (displayId: string): Promise<string> => {
100
+ try {
101
+ const { thread_id } = await resolveThreadForDisplay({
102
+ composable: '$fetch',
103
+ path: { tenant_id: tenantId.value!, display_id: displayId },
104
+ })
105
+ return thread_id
106
+ }
107
+ catch (error) {
108
+ console.warn('No AI-Hub thread found for display', displayId, error)
109
+ return ''
110
+ }
111
+ }
112
+
81
113
  const handleMessage = async (event: MessageEvent) => {
82
114
  console.log('received post event', event)
83
115
  if (event.origin !== runtimeConfig.public.webui.url) return
84
116
 
85
117
  const data = event.data
86
- if (!['show-traces', 'show-sources', 'show-memories', 'set-context'].includes(data.type)) {
118
+ if (!HANDLED_MESSAGE_TYPES.has(data.type)) {
87
119
  console.log('Unknown message type:', data.type)
88
120
  return
89
121
  }
@@ -91,31 +123,41 @@ const handleMessage = async (event: MessageEvent) => {
91
123
  const display_id = data.display_id as string
92
124
  if (!display_id) return
93
125
 
94
- // Explicit action-button click: the action knows only the display, so resolve its owning thread from the backend
95
- // (the correct per-agent, salted thread the pipe persisted events under).
126
+ // Explicit action-button click: open the requested side panel on the owning thread.
96
127
  const requestedView = VIEW_BY_ACTION[data.type]
97
128
  if (requestedView) {
98
- try {
99
- const { thread_id } = await resolveThreadForDisplay({
100
- composable: '$fetch',
101
- path: { tenant_id: tenantId.value!, display_id },
102
- })
129
+ const thread_id = await resolveThreadId(display_id)
130
+ // Leave the panel as-is rather than erroring when nothing owns the display.
131
+ if (thread_id) {
103
132
  router.push(tenantPath(`/service/openai/${thread_id}/${display_id}/${requestedView}`))
104
133
  }
105
- catch (error) {
106
- // No AI-Hub thread owns this display (e.g. a plain-LLM message) — leave the panel as-is instead of erroring.
107
- console.warn('No AI-Hub thread found for display', display_id, error)
108
- }
109
134
  return
110
135
  }
111
136
 
112
- // set-context: the pipe pushes the correct thread_id as each message streams; keep an already-open panel synced.
137
+ // set-model-context: the inlet filter, which runs on every turn whatever routes it — including
138
+ // a plain model, which reaches the API through OpenWebUI's own connection and no pipe at all.
139
+ // It describes the turn from scratch, thread included, because a value it does not set is a
140
+ // value the previous turn would otherwise lend to a report about this one.
141
+ if (data.type === 'set-model-context') {
142
+ setOpenWebUIContext({
143
+ threadId: '',
144
+ displayId: display_id,
145
+ agentClass: (data.agent_class as string) ?? '',
146
+ agentName: (data.agent_name as string) ?? '',
147
+ model: (data.model as string) ?? '',
148
+ })
149
+ return
150
+ }
151
+
152
+ // set-context: the agent pipe, which alone knows the thread its events are persisted under.
153
+ const thread_id = (data.thread_id as string) ?? ''
154
+ updateOpenWebUIContext({ threadId: thread_id, displayId: display_id })
155
+
156
+ // Keep an already-open panel synced. Only a resolved thread has a panel to sync to.
157
+ if (!thread_id) return
113
158
  const view = openPanelView()
114
159
  if (view) {
115
- const thread_id = data.thread_id as string
116
- if (thread_id) {
117
- router.push(tenantPath(`/service/openai/${thread_id}/${display_id}/${view}`))
118
- }
160
+ router.push(tenantPath(`/service/openai/${thread_id}/${display_id}/${view}`))
119
161
  }
120
162
  }
121
163
 
@@ -127,5 +169,7 @@ onMounted(() => {
127
169
  // Clean up event listener when component is unmounted
128
170
  onBeforeUnmount(() => {
129
171
  window.removeEventListener('message', handleMessage)
172
+ // The conversation is no longer on screen, so a report raised elsewhere must not claim it.
173
+ clearOpenWebUIContext()
130
174
  })
131
175
  </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <StructuralScreen>
3
+ <template #top>
4
+ <div class="flex flex-col gap-1">
5
+ <h1 class="text-2xl font-semibold">
6
+ {{ t('userFiles.title') }}
7
+ </h1>
8
+ <p class="text-sm text-surface-500">
9
+ {{ t('userFiles.description') }}
10
+ </p>
11
+ </div>
12
+ </template>
13
+ <UserFilesBrowser />
14
+ </StructuralScreen>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ const { t } = useI18n()
19
+ </script>
@@ -1,4 +1,5 @@
1
1
  import { client } from '@core/sdk/client/client.gen'
2
+ import { createHtmlResponseGuard } from '@core/utils/apiResponseGuard'
2
3
 
3
4
  // Configures the @swiss-ai-hub/web SDK client (`@core/sdk/client`) that every
4
5
  // composable shipped in this layer uses.
@@ -31,6 +32,10 @@ export default defineNuxtPlugin((nuxtApp) => {
31
32
  const locale = (nuxtApp.$i18n as { locale?: { value?: string } } | undefined)?.locale?.value
32
33
  if (locale) options.headers.set('lang', locale)
33
34
  },
35
+ // Both app.vue files re-run setConfig and mergeConfigs is a shallow spread,
36
+ // but neither sets `onResponse` — so this hook survives in @swiss-ai-hub/web
37
+ // and in every extender.
38
+ onResponse: createHtmlResponseGuard(nuxtApp),
34
39
  onResponseError: ({ response }) => {
35
40
  console.error('AI-Hub API error', response.status, response._data?.detail)
36
41
  },
@@ -7,6 +7,14 @@ export default defineNuxtPlugin(async ({ $i18n, $router }) => {
7
7
 
8
8
  // Keycloak-compatible OIDC configuration
9
9
  // Authority URL is the Keycloak realm URL (e.g., http://localhost:8180/realms/aihub)
10
+ //
11
+ // The locale in the three redirect URIs below is frozen at bootstrap: this
12
+ // plugin never re-runs when the user switches language (that is a client-side
13
+ // route push). They are bootstrap defaults only -- composables/auth/useAuth.ts
14
+ // overrides redirect_uri and post_logout_redirect_uri per call with the locale
15
+ // that is live at that moment. silent_redirect_uri keeps the bootstrap value
16
+ // because automaticSilentRenew reads it from here, and /auth/renew renders
17
+ // nothing the user sees.
10
18
  const auth = new UserManager({
11
19
  authority: config.public.oidc.authorityUrl,
12
20
  client_id: config.public.oidc.clientId,
@@ -33,13 +41,12 @@ export default defineNuxtPlugin(async ({ $i18n, $router }) => {
33
41
  console.log('Access token expiring, attempting silent renewal')
34
42
  })
35
43
 
36
- auth.events.addAccessTokenExpired(() => {
37
- console.log('Access token expired')
38
- // Redirect to login when token expires and cannot be renewed
39
- const locale = $i18n.locale.value
40
- $router.push(`/${locale}/auth/login`)
41
- })
42
-
44
+ // No addAccessTokenExpired handler on purpose. For a stored user that is
45
+ // already expired it fires ~1s after load, while middleware/auth.global.ts is
46
+ // still refreshing that same session; a redirect from here races that initial
47
+ // navigation (login bounce, or a fatal 500 via home-redirect). A session that
48
+ // really cannot be renewed ends up in addSilentRenewError below or in the
49
+ // middleware's own refresh.
43
50
  auth.events.addSilentRenewError(async (error) => {
44
51
  console.error('Silent renew error:', error)
45
52
  // Refresh token rejected (e.g. Keycloak invalidated it): drop the dead