@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex h-full flex-col gap-3">
|
|
3
|
+
<div class="flex items-center justify-between gap-2">
|
|
4
|
+
<p class="truncate font-semibold">
|
|
5
|
+
{{ entry.name }}
|
|
6
|
+
</p>
|
|
7
|
+
<div class="flex gap-1">
|
|
8
|
+
<Button
|
|
9
|
+
v-tooltip.bottom="t('userFiles.actions.download')"
|
|
10
|
+
icon="pi pi-download"
|
|
11
|
+
size="small"
|
|
12
|
+
text
|
|
13
|
+
@click="download(entry.path, entry.name)"
|
|
14
|
+
/>
|
|
15
|
+
<Button
|
|
16
|
+
v-tooltip.bottom="t('userFiles.actions.close')"
|
|
17
|
+
icon="pi pi-times"
|
|
18
|
+
size="small"
|
|
19
|
+
text
|
|
20
|
+
@click="emit('close')"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<ProgressBar
|
|
25
|
+
v-if="isLoading"
|
|
26
|
+
mode="indeterminate"
|
|
27
|
+
style="height: 2px"
|
|
28
|
+
/>
|
|
29
|
+
<img
|
|
30
|
+
v-else-if="kind === 'image' && objectUrl"
|
|
31
|
+
:src="objectUrl"
|
|
32
|
+
:alt="entry.name"
|
|
33
|
+
class="max-h-[70vh] self-start rounded border border-surface-200 object-contain dark:border-surface-700"
|
|
34
|
+
>
|
|
35
|
+
<iframe
|
|
36
|
+
v-else-if="kind === 'pdf' && objectUrl"
|
|
37
|
+
:src="objectUrl"
|
|
38
|
+
:title="entry.name"
|
|
39
|
+
class="h-[70vh] w-full rounded border border-surface-200 dark:border-surface-700"
|
|
40
|
+
/>
|
|
41
|
+
<pre
|
|
42
|
+
v-else-if="kind === 'text'"
|
|
43
|
+
class="max-h-[70vh] overflow-auto whitespace-pre-wrap rounded border border-surface-200 bg-surface-0 p-3 text-xs dark:border-surface-700 dark:bg-surface-900"
|
|
44
|
+
>{{ text }}</pre>
|
|
45
|
+
<p
|
|
46
|
+
v-else
|
|
47
|
+
class="text-sm text-surface-500"
|
|
48
|
+
>
|
|
49
|
+
{{ t('userFiles.preview.unavailable') }}
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
import type { FileEntryDto } from '@core/sdk/client'
|
|
56
|
+
|
|
57
|
+
const props = defineProps<{ entry: FileEntryDto }>()
|
|
58
|
+
const emit = defineEmits<{ close: [] }>()
|
|
59
|
+
const { t } = useI18n()
|
|
60
|
+
const { fetchBlob, download } = useUserFileContent()
|
|
61
|
+
|
|
62
|
+
const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp'])
|
|
63
|
+
const TEXT_EXTENSIONS = new Set(['txt', 'md', 'csv', 'json', 'log', 'py', 'yaml', 'yml', 'xml', 'html', 'js', 'ts', 'sql'])
|
|
64
|
+
const MAX_TEXT_BYTES = 2 * 1024 * 1024
|
|
65
|
+
|
|
66
|
+
const objectUrl = ref<string | null>(null)
|
|
67
|
+
const text = ref('')
|
|
68
|
+
const isLoading = ref(false)
|
|
69
|
+
|
|
70
|
+
const kind = computed(() => {
|
|
71
|
+
const extension = props.entry.name.split('.').pop()?.toLowerCase() ?? ''
|
|
72
|
+
if (IMAGE_EXTENSIONS.has(extension)) return 'image'
|
|
73
|
+
if (extension === 'pdf') return 'pdf'
|
|
74
|
+
if (TEXT_EXTENSIONS.has(extension) && (props.entry.size ?? 0) <= MAX_TEXT_BYTES) return 'text'
|
|
75
|
+
return 'none'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Only the latest load may touch the preview: one overtaken by another selection or by unmounting is dropped.
|
|
79
|
+
let loadGeneration = 0
|
|
80
|
+
|
|
81
|
+
const revokeObjectUrl = () => {
|
|
82
|
+
if (objectUrl.value) URL.revokeObjectURL(objectUrl.value)
|
|
83
|
+
objectUrl.value = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const load = async () => {
|
|
87
|
+
const generation = ++loadGeneration
|
|
88
|
+
const previewKind = kind.value
|
|
89
|
+
revokeObjectUrl()
|
|
90
|
+
text.value = ''
|
|
91
|
+
isLoading.value = previewKind !== 'none'
|
|
92
|
+
if (previewKind === 'none') return
|
|
93
|
+
try {
|
|
94
|
+
const blob = await fetchBlob(props.entry.path)
|
|
95
|
+
const content = previewKind === 'text' ? await blob.text() : null
|
|
96
|
+
if (generation !== loadGeneration) return
|
|
97
|
+
if (content === null) objectUrl.value = URL.createObjectURL(blob)
|
|
98
|
+
else text.value = content
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (generation === loadGeneration) isLoading.value = false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
watch(() => props.entry.path, load, { immediate: true })
|
|
106
|
+
onBeforeUnmount(() => {
|
|
107
|
+
loadGeneration++
|
|
108
|
+
revokeObjectUrl()
|
|
109
|
+
})
|
|
110
|
+
</script>
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
export const useAuth = () => {
|
|
2
|
+
// The UserManager in plugins/oidc-client.ts is built once at bootstrap, so the
|
|
3
|
+
// locale baked into its redirect URIs is whatever the app started in. Switching
|
|
4
|
+
// language is a client-side route push, so those URIs go stale and a round trip
|
|
5
|
+
// through Keycloak lands back on the bootstrap locale -- which @nuxtjs/i18n then
|
|
6
|
+
// writes into the `i18n_redirected` cookie, destroying the user's choice.
|
|
7
|
+
// Passing the URIs per call keeps them tied to the locale that is live *now*.
|
|
8
|
+
const localeRedirectUri = (path: string) => {
|
|
9
|
+
const { $i18n } = useNuxtApp()
|
|
10
|
+
return `${globalThis.location.origin}/${$i18n.locale.value}/auth/${path}`
|
|
11
|
+
}
|
|
12
|
+
|
|
2
13
|
const login = (idpHint?: string) => {
|
|
3
|
-
const { $auth } = useNuxtApp()
|
|
14
|
+
const { $auth, $i18n } = useNuxtApp()
|
|
4
15
|
const extraQueryParams = idpHint ? { kc_idp_hint: idpHint } : {}
|
|
5
|
-
$auth.signinRedirect({
|
|
16
|
+
$auth.signinRedirect({
|
|
17
|
+
extraQueryParams,
|
|
18
|
+
redirect_uri: localeRedirectUri('callback'),
|
|
19
|
+
// Renders Keycloak's own login page in the user's language.
|
|
20
|
+
ui_locales: $i18n.locale.value,
|
|
21
|
+
})
|
|
6
22
|
}
|
|
7
23
|
|
|
8
24
|
const logout = async () => {
|
|
9
25
|
const { $auth } = useNuxtApp()
|
|
10
|
-
await $auth.signoutRedirect()
|
|
26
|
+
await $auth.signoutRedirect({ post_logout_redirect_uri: localeRedirectUri('login') })
|
|
11
27
|
}
|
|
12
28
|
|
|
13
29
|
const getUser = async () => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useDocumentVisibility, useIntervalFn } from '@vueuse/core'
|
|
2
|
+
|
|
3
|
+
import type { DocumentDto } from '@core/sdk/client'
|
|
4
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
5
|
+
|
|
6
|
+
// Ingestion runs asynchronously in Dagster and nothing pushes its completion to the UI, so a freshly uploaded
|
|
7
|
+
// document would stay "Processing" until the next focus or remount. Polling runs only while a row on the page is
|
|
8
|
+
// still pending and the tab is visible, so a fully ingested page costs nothing.
|
|
9
|
+
const INGESTION_POLL_INTERVAL_MS = 5_000
|
|
10
|
+
|
|
11
|
+
export function useDocumentIngestionPolling(
|
|
12
|
+
documents: MaybeRefOrGetter<DocumentDto[]>,
|
|
13
|
+
refetch: () => Promise<unknown>,
|
|
14
|
+
isFetching: MaybeRefOrGetter<boolean>,
|
|
15
|
+
) {
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
const { tenantId } = useTenant()
|
|
18
|
+
const queryCache = useQueryCache()
|
|
19
|
+
const visibility = useDocumentVisibility()
|
|
20
|
+
const { isScheduled } = useScheduledDeletions(
|
|
21
|
+
() => route.params.db as string,
|
|
22
|
+
() => route.params.namespace as string,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// A row being deleted never becomes ingested and its cleanup can take hours, so it must not keep the page polling.
|
|
26
|
+
const pendingIds = computed(() => new Set(
|
|
27
|
+
toValue(documents)
|
|
28
|
+
.filter(document => !document.is_ingested && !isScheduled(document.id))
|
|
29
|
+
.map(document => document.id),
|
|
30
|
+
))
|
|
31
|
+
|
|
32
|
+
// refetch() aborts the request still in flight, so a list slower than the interval would never land. Skipping the
|
|
33
|
+
// tick lets it finish instead.
|
|
34
|
+
const { pause, resume } = useIntervalFn(() => {
|
|
35
|
+
if (!toValue(isFetching)) {
|
|
36
|
+
refetch()
|
|
37
|
+
}
|
|
38
|
+
}, INGESTION_POLL_INTERVAL_MS, { immediate: false })
|
|
39
|
+
|
|
40
|
+
watch(
|
|
41
|
+
[() => pendingIds.value.size > 0, visibility],
|
|
42
|
+
([hasPendingDocuments, visibilityState]) => {
|
|
43
|
+
if (hasPendingDocuments && visibilityState === 'visible') {
|
|
44
|
+
resume()
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
pause()
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{ immediate: true },
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// The detail, nodes and summary queries of a document share its key prefix and are cached for minutes, so an open
|
|
54
|
+
// detail view would keep showing the pre-ingestion state without this. A document that left the page before we saw
|
|
55
|
+
// it finish is refreshed too; invalidating one that is gone is harmless.
|
|
56
|
+
watch(pendingIds, (currentPendingIds, previousPendingIds) => {
|
|
57
|
+
const settledIds = [...(previousPendingIds ?? [])].filter(documentId => !currentPendingIds.has(documentId))
|
|
58
|
+
for (const documentId of settledIds) {
|
|
59
|
+
queryCache.invalidateQueries({
|
|
60
|
+
key: ['tenant', tenantId.value, 'knowledge', 'databases', route.params.db as string, 'namespaces', route.params.namespace as string, 'documents', documentId],
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -88,11 +88,14 @@ export const useDocuments = defineQuery(() => {
|
|
|
88
88
|
// Only show loading on initial load, not during refetch/search when we have data
|
|
89
89
|
const isLoading = computed(() => documentsQuery.asyncStatus.value === 'loading' && !documentsQuery.state.value?.data)
|
|
90
90
|
const isFetching = computed(() => documentsQuery.asyncStatus.value === 'loading')
|
|
91
|
+
// Only a changed search, sort or page shows the previous results as placeholder; a background refetch does not.
|
|
92
|
+
const isSearching = computed(() => isFetching.value && documentsQuery.isPlaceholderData.value)
|
|
91
93
|
|
|
92
94
|
return {
|
|
93
95
|
documents,
|
|
94
96
|
isLoading,
|
|
95
97
|
isFetching,
|
|
98
|
+
isSearching,
|
|
96
99
|
pagination: paginationMeta,
|
|
97
100
|
currentPage,
|
|
98
101
|
pageSize,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getSourcePipelines, type SourcePipelineDto } from '@core/sdk/client'
|
|
2
|
+
import { minutesToMilliseconds } from 'date-fns'
|
|
3
|
+
|
|
4
|
+
export const useSourcePipelines = defineQuery(() => {
|
|
5
|
+
const { tenantId } = useTenant()
|
|
6
|
+
|
|
7
|
+
const { data: sourcePipelines, isPending: sourcePipelinesAreLoading } = useQuery<SourcePipelineDto[]>({
|
|
8
|
+
key: () => ['tenant', tenantId.value, 'source-pipelines'],
|
|
9
|
+
staleTime: minutesToMilliseconds(5),
|
|
10
|
+
enabled: useTenantReady(),
|
|
11
|
+
query: async () => {
|
|
12
|
+
return await getSourcePipelines({
|
|
13
|
+
composable: '$fetch',
|
|
14
|
+
path: { tenant_id: tenantId.value! },
|
|
15
|
+
})
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
sourcePipelines,
|
|
21
|
+
sourcePipelinesAreLoading,
|
|
22
|
+
}
|
|
23
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { updateDatabaseSource, type UpdateDatabaseSourceRequest } from '@core/sdk/client'
|
|
2
|
+
|
|
3
|
+
export const useUpdateDatabaseSource = defineMutation(() => {
|
|
4
|
+
const queryCache = useQueryCache()
|
|
5
|
+
const { tenantId } = useTenant()
|
|
6
|
+
|
|
7
|
+
return useMutation({
|
|
8
|
+
mutation: (params: { database: string, tenantId: string, request: UpdateDatabaseSourceRequest }) =>
|
|
9
|
+
updateDatabaseSource({
|
|
10
|
+
composable: '$fetch',
|
|
11
|
+
body: params.request,
|
|
12
|
+
path: {
|
|
13
|
+
tenant_id: params.tenantId,
|
|
14
|
+
database: params.database,
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
onSuccess: () => {
|
|
18
|
+
queryCache.invalidateQueries({ key: ['tenant', tenantId.value, 'knowledge'] })
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -31,7 +31,30 @@ import {
|
|
|
31
31
|
EventDisplayBaseRetrieveMemoryEvent,
|
|
32
32
|
EventDisplayBaseStoreMemoryEvent,
|
|
33
33
|
EventDisplayAddMemoryToChatHistoryEvent,
|
|
34
|
+
EventDisplayContextComposedEvent,
|
|
34
35
|
EventDisplayMetaQuestionDetectedEvent,
|
|
36
|
+
EventDisplayContextualizeConversationEvent,
|
|
37
|
+
EventDisplayConversationContextualizedEvent,
|
|
38
|
+
EventDisplayComposeContextEvent,
|
|
39
|
+
EventDisplayCompleteConversationEvent,
|
|
40
|
+
EventDisplayNotAMetaQuestionEvent,
|
|
41
|
+
EventDisplayRecallMemoryEvent,
|
|
42
|
+
EventDisplayMemoryRecalledEvent,
|
|
43
|
+
EventDisplayMemoryStorageRequestedEvent,
|
|
44
|
+
EventDisplaySearchKnowledgeEvent,
|
|
45
|
+
EventDisplayKnowledgeSearchedEvent,
|
|
46
|
+
EventDisplayReadAttachedFilesEvent,
|
|
47
|
+
EventDisplayAttachedFilesReadEvent,
|
|
48
|
+
EventDisplayAttachedFileEvent,
|
|
49
|
+
EventDisplayRunToolLoopEvent,
|
|
50
|
+
EventDisplayToolLoopIterationEvent,
|
|
51
|
+
EventDisplayToolCallsDecidedEvent,
|
|
52
|
+
EventDisplayToolCallApprovedEvent,
|
|
53
|
+
EventDisplayToolResultEvent,
|
|
54
|
+
EventDisplayToolLoopStatusEvent,
|
|
55
|
+
EventDisplayToolLoopCondensedEvent,
|
|
56
|
+
EventDisplayToolLoopFinishedEvent,
|
|
57
|
+
EventDisplaySandboxFileDisplayedEvent,
|
|
35
58
|
} from '#components'
|
|
36
59
|
|
|
37
60
|
export const useEventComponent = () => {
|
|
@@ -74,7 +97,30 @@ export const useEventComponent = () => {
|
|
|
74
97
|
BaseRetrieveMemoryEvent: EventDisplayBaseRetrieveMemoryEvent,
|
|
75
98
|
BaseStoreMemoryEvent: EventDisplayBaseStoreMemoryEvent,
|
|
76
99
|
AddMemoryToChatHistoryEvent: EventDisplayAddMemoryToChatHistoryEvent,
|
|
100
|
+
ContextComposedEvent: EventDisplayContextComposedEvent,
|
|
77
101
|
MetaQuestionDetectedEvent: EventDisplayMetaQuestionDetectedEvent,
|
|
102
|
+
ContextualizeConversationEvent: EventDisplayContextualizeConversationEvent,
|
|
103
|
+
ConversationContextualizedEvent: EventDisplayConversationContextualizedEvent,
|
|
104
|
+
ComposeContextEvent: EventDisplayComposeContextEvent,
|
|
105
|
+
CompleteConversationEvent: EventDisplayCompleteConversationEvent,
|
|
106
|
+
NotAMetaQuestionEvent: EventDisplayNotAMetaQuestionEvent,
|
|
107
|
+
RecallMemoryEvent: EventDisplayRecallMemoryEvent,
|
|
108
|
+
MemoryRecalledEvent: EventDisplayMemoryRecalledEvent,
|
|
109
|
+
MemoryStorageRequestedEvent: EventDisplayMemoryStorageRequestedEvent,
|
|
110
|
+
SearchKnowledgeEvent: EventDisplaySearchKnowledgeEvent,
|
|
111
|
+
KnowledgeSearchedEvent: EventDisplayKnowledgeSearchedEvent,
|
|
112
|
+
ReadAttachedFilesEvent: EventDisplayReadAttachedFilesEvent,
|
|
113
|
+
AttachedFilesReadEvent: EventDisplayAttachedFilesReadEvent,
|
|
114
|
+
AttachedFileEvent: EventDisplayAttachedFileEvent,
|
|
115
|
+
RunToolLoopEvent: EventDisplayRunToolLoopEvent,
|
|
116
|
+
ToolLoopIterationEvent: EventDisplayToolLoopIterationEvent,
|
|
117
|
+
ToolCallsDecidedEvent: EventDisplayToolCallsDecidedEvent,
|
|
118
|
+
ToolCallApprovedEvent: EventDisplayToolCallApprovedEvent,
|
|
119
|
+
ToolResultEvent: EventDisplayToolResultEvent,
|
|
120
|
+
ToolLoopStatusEvent: EventDisplayToolLoopStatusEvent,
|
|
121
|
+
ToolLoopCondensedEvent: EventDisplayToolLoopCondensedEvent,
|
|
122
|
+
ToolLoopFinishedEvent: EventDisplayToolLoopFinishedEvent,
|
|
123
|
+
SandboxFileDisplayedEvent: EventDisplaySandboxFileDisplayedEvent,
|
|
78
124
|
}
|
|
79
125
|
const exact_match = mapping[event.event._event_name]
|
|
80
126
|
if (exact_match) {
|
|
@@ -268,6 +268,8 @@ const EXCLUDED_FIELDS = new Set([
|
|
|
268
268
|
'nullable', // Wrapper-level signal for the transform; never a FormKit/PrimeVue prop
|
|
269
269
|
'defaultEnabled', // Wrapper-level signal (initial nullable-toggle state); never a FormKit prop
|
|
270
270
|
'default_enabled', // snake_case form of the above
|
|
271
|
+
'toggleLabel', // Rendered on the synthetic nullable toggle, never on the element itself
|
|
272
|
+
'toggleHelp', // Rendered on the synthetic nullable toggle, never on the element itself
|
|
271
273
|
// Backend serialises the Pydantic default into element.value (form duality). FormKit pushes
|
|
272
274
|
// schema `value` up to the parent v-model on input registration, which would clobber the
|
|
273
275
|
// loaded data with the backend default. Defaults belong in data, seeded via seedFormDefaults.
|
|
@@ -286,6 +288,15 @@ function buildNodeProperties(
|
|
|
286
288
|
return buildLocaleInputProperties(element, label, locale)
|
|
287
289
|
}
|
|
288
290
|
|
|
291
|
+
// Markup, not an input: an `$el` element carries no formkit type, and giving it one anyway
|
|
292
|
+
// emitted `$formkit: undefined` alongside the tag, which FormKit rendered as an empty box.
|
|
293
|
+
const tag = element.$el ?? element.el
|
|
294
|
+
if (!formkitType && tag) {
|
|
295
|
+
const markup: Record<string, unknown> = { $el: tag as string }
|
|
296
|
+
if (element.attrs) markup.attrs = element.attrs
|
|
297
|
+
return markup
|
|
298
|
+
}
|
|
299
|
+
|
|
289
300
|
// `preserve: true` keeps the input's value in the form context when an `if:`
|
|
290
301
|
// condition (nullable toggle or backend-supplied `condition_if`) unmounts it.
|
|
291
302
|
const cleanNode: Record<string, unknown> = { $formkit: formkitType, preserve: true }
|
|
@@ -362,10 +373,13 @@ function buildNullableToggleNode(
|
|
|
362
373
|
element: FormElement,
|
|
363
374
|
label: string | undefined,
|
|
364
375
|
help: string | undefined,
|
|
376
|
+
toggleId: string | undefined,
|
|
377
|
+
locale: string,
|
|
365
378
|
): Record<string, unknown> {
|
|
366
379
|
const fieldName = element.name as string
|
|
367
|
-
const toggleId = nullableToggleId(element)
|
|
368
380
|
const gatingCondition = element.if as string | undefined
|
|
381
|
+
const toggleLabel = getLocalizedString(element.toggleLabel, locale) ?? (label ? `Enable ${label}` : 'Enable')
|
|
382
|
+
const toggleHelp = getLocalizedString(element.toggleHelp, locale) ?? help
|
|
369
383
|
return {
|
|
370
384
|
$formkit: 'primeCheckbox',
|
|
371
385
|
// `preserve: true` for the same reason the gated input itself carries it: when this toggle's own
|
|
@@ -373,10 +387,13 @@ function buildNullableToggleNode(
|
|
|
373
387
|
// state seeded from the saved value is lost — an already-configured field then remounts reading "off".
|
|
374
388
|
preserve: true,
|
|
375
389
|
name: nullableToggleName(fieldName),
|
|
376
|
-
id
|
|
377
|
-
key
|
|
378
|
-
|
|
379
|
-
...(
|
|
390
|
+
// Repeater rows share one schema object, so a fixed id would register every row's toggle under the
|
|
391
|
+
// same key in FormKit's single global node registry. Those rows pass no id and let FormKit assign a
|
|
392
|
+
// unique one per instance; they gate on row data instead of `$get`, so nothing looks the id up.
|
|
393
|
+
...(toggleId ? { id: toggleId } : {}),
|
|
394
|
+
key: toggleId ?? nullableToggleName(fieldName),
|
|
395
|
+
label: toggleLabel,
|
|
396
|
+
...(toggleHelp ? { help: toggleHelp } : {}),
|
|
380
397
|
...(gatingCondition ? { if: gatingCondition } : {}),
|
|
381
398
|
binary: true,
|
|
382
399
|
}
|
|
@@ -385,16 +402,19 @@ function buildNullableToggleNode(
|
|
|
385
402
|
/**
|
|
386
403
|
* `help` is only supplied for groups: a group's own node renders no help, so the toggle is the
|
|
387
404
|
* only place to hang it. Nullable leaves already render their help on the input itself — passing
|
|
388
|
-
* it here too would print the same sentence twice.
|
|
405
|
+
* it here too would print the same sentence twice. An element's own `toggleHelp` is meant for the
|
|
406
|
+
* toggle alone, so it is rendered there for leaves and groups alike.
|
|
389
407
|
*/
|
|
390
408
|
function applyNullableToggle(
|
|
391
409
|
element: FormElement,
|
|
392
410
|
baseNode: FormKitSchemaNode | FormKitSchemaNode[],
|
|
393
411
|
label: string | undefined,
|
|
412
|
+
toggleId: string | undefined,
|
|
413
|
+
locale: string,
|
|
394
414
|
help?: string,
|
|
395
415
|
): FormKitSchemaNode[] {
|
|
396
416
|
const nodeArray = Array.isArray(baseNode) ? baseNode : [baseNode]
|
|
397
|
-
return [buildNullableToggleNode(element, label, help) as FormKitSchemaNode, ...nodeArray]
|
|
417
|
+
return [buildNullableToggleNode(element, label, help, toggleId, locale) as FormKitSchemaNode, ...nodeArray]
|
|
398
418
|
}
|
|
399
419
|
|
|
400
420
|
function gateElement(element: FormElement, toggleCondition: string): FormElement {
|
|
@@ -406,6 +426,22 @@ function gateElement(element: FormElement, toggleCondition: string): FormElement
|
|
|
406
426
|
* Handles groups specially by wrapping them in fieldsets when they have labels.
|
|
407
427
|
* Skips repeater elements (they are handled separately).
|
|
408
428
|
*/
|
|
429
|
+
/**
|
|
430
|
+
* Transforms an element's children, letting text through untouched.
|
|
431
|
+
*
|
|
432
|
+
* A child may be prose rather than another element — `$el` nodes carry their content that way and
|
|
433
|
+
* the schema takes a bare string there (see `buildFieldWarningNode`). Recursing into a string
|
|
434
|
+
* yielded nothing, so an element declaring text rendered as an empty tag.
|
|
435
|
+
*/
|
|
436
|
+
function transformChildren(
|
|
437
|
+
children: unknown,
|
|
438
|
+
transform: (child: FormElement) => FormKitSchemaNode | FormKitSchemaNode[],
|
|
439
|
+
): FormKitSchemaNode[] {
|
|
440
|
+
if (!children) return []
|
|
441
|
+
const list = Array.isArray(children) ? children : [children]
|
|
442
|
+
return list.flatMap(child => (typeof child === 'string' ? child : transform(child as FormElement))) as FormKitSchemaNode[]
|
|
443
|
+
}
|
|
444
|
+
|
|
409
445
|
export function transformElementToSchema(
|
|
410
446
|
element: FormElement,
|
|
411
447
|
options: TransformOptions = {},
|
|
@@ -417,9 +453,7 @@ export function transformElementToSchema(
|
|
|
417
453
|
|
|
418
454
|
const { locale = 'en', labelTransform, optionsResolver, fieldWarning } = options
|
|
419
455
|
|
|
420
|
-
const children = (element.children
|
|
421
|
-
child => transformElementToSchema(child, options),
|
|
422
|
-
) as FormKitSchemaNode[]
|
|
456
|
+
const children = transformChildren(element.children, child => transformElementToSchema(child, options))
|
|
423
457
|
|
|
424
458
|
let label = getLocalizedString(element.label, locale)
|
|
425
459
|
if (label && labelTransform) {
|
|
@@ -427,7 +461,8 @@ export function transformElementToSchema(
|
|
|
427
461
|
}
|
|
428
462
|
|
|
429
463
|
const isNullable = element.nullable === true
|
|
430
|
-
const
|
|
464
|
+
const toggleId = isNullable ? nullableToggleId(element) : undefined
|
|
465
|
+
const toggleCondition = isNullable ? `$get(${toggleId}).value` : undefined
|
|
431
466
|
|
|
432
467
|
const warningText = fieldWarning?.(element)
|
|
433
468
|
const warningNode = warningText ? buildFieldWarningNode(element, warningText) : undefined
|
|
@@ -436,7 +471,9 @@ export function transformElementToSchema(
|
|
|
436
471
|
const gatedElement = isNullable ? gateElement(element, toggleCondition!) : element
|
|
437
472
|
const groupNode = createGroupNode(gatedElement, children, label)
|
|
438
473
|
if (!isNullable) return withFieldWarning(groupNode, warningNode, false)
|
|
439
|
-
const toggledNodes = applyNullableToggle(
|
|
474
|
+
const toggledNodes = applyNullableToggle(
|
|
475
|
+
element, groupNode, label, toggleId, locale, getLocalizedString(element.help, locale),
|
|
476
|
+
)
|
|
440
477
|
return withFieldWarning(toggledNodes, warningNode, true)
|
|
441
478
|
}
|
|
442
479
|
|
|
@@ -444,7 +481,7 @@ export function transformElementToSchema(
|
|
|
444
481
|
if (children.length > 0) cleanNode.children = children
|
|
445
482
|
if (isNullable) {
|
|
446
483
|
cleanNode.if = combineConditions(toggleCondition!, element.if as string | undefined)
|
|
447
|
-
const toggledNodes = applyNullableToggle(element, cleanNode as FormKitSchemaNode, label)
|
|
484
|
+
const toggledNodes = applyNullableToggle(element, cleanNode as FormKitSchemaNode, label, toggleId, locale)
|
|
448
485
|
return withFieldWarning(toggledNodes, warningNode, true)
|
|
449
486
|
}
|
|
450
487
|
|
|
@@ -478,33 +515,47 @@ function buildLeafNodeForRepeater(
|
|
|
478
515
|
|
|
479
516
|
/**
|
|
480
517
|
* Transforms a form element for use inside a repeater's children schema.
|
|
518
|
+
*
|
|
519
|
+
* A nullable element here gates on its own row's data (`$__<field>__enabled`) rather than on
|
|
520
|
+
* `$get(<id>).value` as it does outside a repeater. Every row renders the one shared
|
|
521
|
+
* `childrenSchema` object, so a schema-level id is the same string in all of them — and FormKit
|
|
522
|
+
* resolves `$get` through a single global registry keyed by that id, which holds one node for the
|
|
523
|
+
* whole repeater (the last row to mount; a row unmounting deletes the key outright). The gate then
|
|
524
|
+
* answered for an arbitrary row instead of the one whose checkbox was clicked, so ticking "Enable X"
|
|
525
|
+
* mounted nothing. A data reference resolves against the `:data="modelValue[index]"` each row is
|
|
526
|
+
* rendered with, which is per row by construction. `dataPath` is the dotted prefix of the enclosing
|
|
527
|
+
* groups, since a nested group's toggle is seeded inside that group's object, not at the row root.
|
|
481
528
|
*/
|
|
482
529
|
export function transformElementForRepeater(
|
|
483
530
|
element: FormElement,
|
|
484
531
|
locale = 'en',
|
|
532
|
+
dataPath = '',
|
|
485
533
|
): FormKitSchemaNode | FormKitSchemaNode[] {
|
|
486
534
|
if (!element) return []
|
|
487
535
|
|
|
488
536
|
const formkitType = getFormkitType(element)
|
|
537
|
+
const childPath = formkitType === 'group' ? `${dataPath}${element.name as string}.` : dataPath
|
|
489
538
|
const children = (element.children as FormElement[] || []).flatMap(
|
|
490
|
-
child => transformElementForRepeater(child, locale),
|
|
539
|
+
child => transformElementForRepeater(child, locale, childPath),
|
|
491
540
|
) as FormKitSchemaNode[]
|
|
492
541
|
|
|
493
542
|
const label = getLocalizedString(element.label, locale)
|
|
494
543
|
const isNullable = element.nullable === true
|
|
495
|
-
const toggleCondition = isNullable
|
|
544
|
+
const toggleCondition = isNullable
|
|
545
|
+
? `$${dataPath}${nullableToggleName(element.name as string)}`
|
|
546
|
+
: undefined
|
|
496
547
|
|
|
497
548
|
if (formkitType === 'group') {
|
|
498
549
|
const gatedElement = isNullable ? gateElement(element, toggleCondition!) : element
|
|
499
550
|
const groupNode = createGroupNode(gatedElement, children, label)
|
|
500
551
|
if (!isNullable) return groupNode
|
|
501
|
-
return applyNullableToggle(element, groupNode, label, getLocalizedString(element.help, locale))
|
|
552
|
+
return applyNullableToggle(element, groupNode, label, undefined, locale, getLocalizedString(element.help, locale))
|
|
502
553
|
}
|
|
503
554
|
|
|
504
555
|
const cleanNode = buildLeafNodeForRepeater(element, formkitType, label, locale, children)
|
|
505
556
|
if (isNullable) {
|
|
506
557
|
cleanNode.if = combineConditions(toggleCondition!, element.if as string | undefined)
|
|
507
|
-
return applyNullableToggle(element, cleanNode as FormKitSchemaNode, label)
|
|
558
|
+
return applyNullableToggle(element, cleanNode as FormKitSchemaNode, label, undefined, locale)
|
|
508
559
|
}
|
|
509
560
|
|
|
510
561
|
return cleanNode as FormKitSchemaNode
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useOpenWebuiDisclaimer(iframe: Ref<HTMLIFrameElement | null>, text: Ref<string>) {
|
|
4
|
+
const config = useRuntimeConfig()
|
|
5
|
+
const origin = new URL(config.public.webui.url).origin
|
|
6
|
+
const type = 'aihub:chat-disclaimer'
|
|
7
|
+
|
|
8
|
+
function sendDisclaimer() {
|
|
9
|
+
iframe.value?.contentWindow?.postMessage({ type, version: 1, text: text.value }, origin)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function onMessage(event: MessageEvent) {
|
|
13
|
+
if (event.origin !== origin || event.source !== iframe.value?.contentWindow) return
|
|
14
|
+
if (event.data?.type === `${type}:ready` && event.data.version === 1) sendDisclaimer()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
watch(text, sendDisclaimer)
|
|
18
|
+
onMounted(() => window.addEventListener('message', onMessage))
|
|
19
|
+
onBeforeUnmount(() => window.removeEventListener('message', onMessage))
|
|
20
|
+
|
|
21
|
+
return { sendDisclaimer }
|
|
22
|
+
}
|
|
@@ -2,19 +2,27 @@ import type { ServiceDto } from '@core/sdk/client'
|
|
|
2
2
|
import type { MenuItem } from 'primevue/menuitem'
|
|
3
3
|
|
|
4
4
|
export const useApps = () => {
|
|
5
|
-
const { suite, suiteIsLoading } = useSuite()
|
|
5
|
+
const { suite, suiteError, suiteIsLoading } = useSuite()
|
|
6
6
|
const router = useRouter()
|
|
7
7
|
const tenantPath = useTenantPath()
|
|
8
8
|
|
|
9
9
|
const apps = computed<MenuItem>(() => {
|
|
10
|
-
|
|
10
|
+
// `services` is non-optional on SuiteDto, so the type system cannot catch a
|
|
11
|
+
// cached value that is not actually a SuiteDto. It used to be reachable: a
|
|
12
|
+
// proxy answering 200 text/html for a downed API landed here as a raw HTML
|
|
13
|
+
// string and `.services.map` threw inside this computed, taking down every
|
|
14
|
+
// page on the default layout. The SDK guard now rejects that response; this
|
|
15
|
+
// stays as the last line of defence.
|
|
16
|
+
const services = Array.isArray(suite.value?.services) ? suite.value.services : []
|
|
17
|
+
|
|
18
|
+
const suiteApps = services.map((service: ServiceDto) => ({
|
|
11
19
|
label: service.name,
|
|
12
20
|
description: service.description,
|
|
13
21
|
icon: service.icon,
|
|
14
22
|
path: service.path,
|
|
15
23
|
isAdmin: service.user_is_admin ?? false,
|
|
16
24
|
} satisfies MenuItem
|
|
17
|
-
))
|
|
25
|
+
))
|
|
18
26
|
return [
|
|
19
27
|
{ icon: 'material-symbols:home', label: 'Home', path: '/' },
|
|
20
28
|
...suiteApps,
|
|
@@ -27,5 +35,6 @@ export const useApps = () => {
|
|
|
27
35
|
return {
|
|
28
36
|
apps,
|
|
29
37
|
appsLoading: suiteIsLoading,
|
|
38
|
+
appsError: suiteError,
|
|
30
39
|
}
|
|
31
40
|
}
|
|
@@ -6,6 +6,7 @@ export const useSuite = defineQuery(() => {
|
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
data: suite,
|
|
9
|
+
error: suiteError,
|
|
9
10
|
isPending: suiteIsLoading,
|
|
10
11
|
} = useQuery<SuiteDto>({
|
|
11
12
|
key: () => ['tenant', tenantId.value, 'suite'],
|
|
@@ -21,6 +22,7 @@ export const useSuite = defineQuery(() => {
|
|
|
21
22
|
|
|
22
23
|
return {
|
|
23
24
|
suite,
|
|
25
|
+
suiteError,
|
|
24
26
|
suiteIsLoading,
|
|
25
27
|
}
|
|
26
28
|
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getIncidentAvailability } from '@core/sdk/client'
|
|
2
|
+
import { hoursToMilliseconds } from 'date-fns'
|
|
3
|
+
|
|
4
|
+
// Asked once per app load, and answered 200 on every deployment: the form endpoint itself answers
|
|
5
|
+
// 404 where reporting is not configured, and the shell's global error handler would toast that at
|
|
6
|
+
// a user who has not clicked anything. The button is drawn only where it leads somewhere.
|
|
7
|
+
export const useIncidentAvailability = defineQuery(() => {
|
|
8
|
+
const { data } = useQuery({
|
|
9
|
+
key: () => ['incident-availability'],
|
|
10
|
+
staleTime: hoursToMilliseconds(1),
|
|
11
|
+
query: async () => await getIncidentAvailability({ composable: '$fetch' }),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const incidentReportingIsAvailable = computed(() => data.value?.enabled === true)
|
|
15
|
+
|
|
16
|
+
return { incidentReportingIsAvailable }
|
|
17
|
+
})
|