@swiss-ai-hub/web 0.321.0 → 0.323.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.
- package/components/Event/Display/AddMemoryToChatHistoryEvent.vue +3 -3
- package/components/Event/Display/AttachedFileEvent.vue +42 -0
- package/components/Event/Display/AttachedFilesReadEvent.vue +22 -0
- package/components/Event/Display/BaseRetrieveMemoryEvent.vue +2 -2
- package/components/Event/Display/BaseStoreMemoryEvent.vue +2 -2
- package/components/Event/Display/CompleteConversationEvent.vue +35 -0
- package/components/Event/Display/ComposeContextEvent.vue +38 -0
- package/components/Event/Display/ContextComposedEvent.vue +60 -0
- package/components/Event/Display/ContextualizeConversationEvent.vue +26 -0
- package/components/Event/Display/ConversationContextualizedEvent.vue +34 -0
- package/components/Event/Display/HumanInTheLoopResponseEvent.vue +11 -1
- package/components/Event/Display/KnowledgeSearchedEvent.vue +33 -0
- package/components/Event/Display/MemoryRecalledEvent.vue +33 -0
- package/components/Event/Display/MemoryStorageRequestedEvent.vue +29 -0
- package/components/Event/Display/NotAMetaQuestionEvent.vue +20 -0
- package/components/Event/Display/Part/Facts.vue +39 -0
- package/components/Event/Display/Part/MessageList.vue +50 -0
- package/components/Event/Display/Part/ToolCalls.vue +58 -0
- package/components/Event/Display/Part/ToolLoopState.vue +36 -0
- package/components/Event/Display/ReadAttachedFilesEvent.vue +28 -0
- package/components/Event/Display/RecallMemoryEvent.vue +27 -0
- package/components/Event/Display/RunToolLoopEvent.vue +34 -0
- package/components/Event/Display/SandboxFileDisplayedEvent.vue +46 -0
- package/components/Event/Display/SearchKnowledgeEvent.vue +31 -0
- package/components/Event/Display/ToolCallApprovedEvent.vue +27 -0
- package/components/Event/Display/ToolCallsDecidedEvent.vue +24 -0
- package/components/Event/Display/ToolLoopCondensedEvent.vue +31 -0
- package/components/Event/Display/ToolLoopFinishedEvent.vue +49 -0
- package/components/Event/Display/ToolLoopIterationEvent.vue +18 -0
- package/components/Event/Display/ToolLoopStatusEvent.vue +27 -0
- package/components/Event/Display/ToolResultEvent.vue +38 -0
- package/components/Event/List/EventList.vue +1 -1
- package/components/FormKit/DynamicConfiguration.vue +6 -1
- package/components/FormKit/KnowledgeCollectionSelector.vue +281 -0
- package/components/FormKit/KnowledgeDatabaseSelector.vue +7 -0
- package/components/FormKit/LocaleInput.vue +2 -0
- package/components/FormKit/SecretFileInput.vue +181 -0
- package/components/FormKit/VectorStoreInput.vue +20 -1
- package/components/Knowledge/Database/CreateModal.vue +91 -2
- package/components/Knowledge/Database/SourceModal.vue +165 -0
- package/components/Knowledge/Document/UploadModal.vue +4 -0
- package/components/Knowledge/Namespace/Card.vue +4 -4
- package/components/Knowledge/Namespace/CreateModal.vue +1 -1
- package/components/Knowledge/Namespace/EditModal.vue +3 -4
- package/components/Role/AccessRulesEditor.vue +1 -1
- package/components/Service/Selection.vue +7 -1
- package/components/Support/IncidentButton.vue +37 -0
- package/components/Support/IncidentDialog.vue +119 -0
- package/components/User/Settings.vue +9 -8
- package/components/UserFiles/Browser.vue +361 -0
- package/components/UserFiles/MoveDialog.vue +77 -0
- package/components/UserFiles/NameDialog.vue +59 -0
- package/components/UserFiles/Preview.vue +110 -0
- package/composables/auth/useAuth.ts +19 -3
- package/composables/document/useDocumentIngestionPolling.ts +64 -0
- package/composables/document/useDocuments.ts +3 -0
- package/composables/document/useSourcePipelines.ts +23 -0
- package/composables/document/useUpdateDatabaseSource.ts +21 -0
- package/composables/event/useEventComponent.ts +46 -0
- package/composables/form/useFormKitTransform.ts +68 -17
- package/composables/openwebui/useOpenWebuiDisclaimer.ts +22 -0
- package/composables/suite/useApps.ts +12 -3
- package/composables/suite/useSuite.ts +2 -0
- package/composables/support/useIncidentAvailability.ts +17 -0
- package/composables/support/useIncidentContext.ts +56 -0
- package/composables/support/useIncidentForm.ts +45 -0
- package/composables/support/useIncidentReport.ts +16 -0
- package/composables/support/useOpenWebUIContext.ts +38 -0
- package/composables/tenant/useChatDisclaimer.ts +24 -0
- package/composables/user/useApplyLocale.ts +23 -0
- package/composables/user/useMyLocale.ts +20 -0
- package/composables/user/useRestorePreferredLocale.ts +38 -0
- package/composables/userFiles/useCreateUserFolder.ts +15 -0
- package/composables/userFiles/useDeleteUserFile.ts +15 -0
- package/composables/userFiles/useMoveUserFile.ts +15 -0
- package/composables/userFiles/useUploadUserFiles.ts +19 -0
- package/composables/userFiles/useUserFileContent.ts +24 -0
- package/composables/userFiles/useUserFiles.ts +25 -0
- package/formkit.config.ts +10 -2
- package/i18n/locales/de.yaml +171 -3
- package/i18n/locales/en.yaml +172 -3
- package/i18n/locales/fr.yaml +173 -3
- package/i18n/locales/it.yaml +173 -3
- package/layouts/default.vue +7 -1
- package/middleware/auth.global.ts +6 -3
- package/middleware/home-redirect.ts +15 -4
- package/package.json +4 -3
- package/pages/[tenant]/service/knowledge/[db]/[namespace].vue +6 -8
- package/pages/[tenant]/service/knowledge.vue +33 -7
- package/pages/[tenant]/service/openai/[thread_id]/[display_id]/files.vue +51 -0
- package/pages/[tenant]/service/openai.vue +61 -17
- package/pages/[tenant]/service/user-knowledge.vue +19 -0
- package/plugins/api-client.client.ts +5 -0
- package/plugins/oidc-client.ts +14 -7
- package/sdk/client/index.ts +171 -1
- package/sdk/client/schemas.gen.ts +18602 -8560
- package/sdk/client/sdk.gen.ts +482 -0
- package/sdk/client/types.gen.ts +13616 -7044
- package/types/EventFact.ts +4 -0
- 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.
|
|
17
|
-
class="pi pi-
|
|
18
|
-
:title="t('knowledge.
|
|
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
|
-
:
|
|
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.
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
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
|
},
|
package/plugins/oidc-client.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|